diff --git a/back/methods/account/recover-password.js b/back/methods/account/recover-password.js
new file mode 100644
index 000000000..ddea76829
--- /dev/null
+++ b/back/methods/account/recover-password.js
@@ -0,0 +1,30 @@
+module.exports = Self => {
+ Self.remoteMethod('recoverPassword', {
+ description: 'Send email to the user',
+ accepts: [
+ {
+ arg: 'email',
+ type: 'string',
+ description: 'The email of user',
+ required: true
+ }
+ ],
+ http: {
+ path: `/recoverPassword`,
+ verb: 'POST'
+ }
+ });
+
+ Self.recoverPassword = async function(email) {
+ const models = Self.app.models;
+
+ try {
+ await models.user.resetPassword({email, emailTemplate: 'recover-password'});
+ } catch (err) {
+ if (err.code === 'EMAIL_NOT_FOUND')
+ return;
+ else
+ throw err;
+ }
+ };
+};
diff --git a/back/methods/account/specs/set-password.spec.js b/back/methods/account/specs/set-password.spec.js
index c76fd52b8..fe71873de 100644
--- a/back/methods/account/specs/set-password.spec.js
+++ b/back/methods/account/specs/set-password.spec.js
@@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server');
-describe('account changePassword()', () => {
+describe('account setPassword()', () => {
it('should throw an error when password does not meet requirements', async() => {
let req = app.models.Account.setPassword(1, 'insecurePass');
diff --git a/back/methods/chat/notifyIssues.js b/back/methods/chat/notifyIssues.js
index 3c4706d07..f618a6fb1 100644
--- a/back/methods/chat/notifyIssues.js
+++ b/back/methods/chat/notifyIssues.js
@@ -32,7 +32,7 @@ module.exports = Self => {
let message = $t(`There's a new urgent ticket:`);
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
tickets.forEach(ticket => {
- message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`;
+ message += `\r\n[ID: ${ticket.number} - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`;
});
const department = await models.Department.findOne({
diff --git a/back/methods/chat/spec/notifyIssue.spec.js b/back/methods/chat/spec/notifyIssue.spec.js
index e20d43142..1aab51793 100644
--- a/back/methods/chat/spec/notifyIssue.spec.js
+++ b/back/methods/chat/spec/notifyIssue.spec.js
@@ -27,7 +27,7 @@ describe('Chat notifyIssue()', () => {
subject: 'Issue title'}
]);
// eslint-disable-next-line max-len
- const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
+ const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: 00001 - Issue title @batman](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName;
diff --git a/back/methods/dms/deleteTrashFiles.js b/back/methods/dms/deleteTrashFiles.js
index 63d7021c5..f14e65e9f 100644
--- a/back/methods/dms/deleteTrashFiles.js
+++ b/back/methods/dms/deleteTrashFiles.js
@@ -51,7 +51,7 @@ module.exports = Self => {
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
await fs.unlink(dstFile);
} catch (err) {
- if (err.code != 'ENOENT')
+ if (err.code != 'ENOENT' && dms.file)
throw err;
}
diff --git a/back/models/account.js b/back/models/account.js
index f74052b5c..c2502380a 100644
--- a/back/models/account.js
+++ b/back/models/account.js
@@ -1,4 +1,7 @@
+/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5');
+const LoopBackContext = require('loopback-context');
+const {Email} = require('vn-print');
module.exports = Self => {
require('../methods/account/login')(Self);
@@ -6,6 +9,7 @@ module.exports = Self => {
require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
+ require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
@@ -27,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password);
});
+ Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
+ if (!ctx.args || !ctx.args.data.email) return;
+ const models = Self.app.models;
+
+ const loopBackContext = LoopBackContext.getCurrentContext();
+ const httpCtx = {req: loopBackContext.active};
+ const httpRequest = httpCtx.req.http.req;
+ const headers = httpRequest.headers;
+ const origin = headers.origin;
+ const url = origin.split(':');
+
+ const userId = ctx.instance.id;
+ const user = await models.user.findById(userId);
+
+ class Mailer {
+ async send(verifyOptions, cb) {
+ const params = {
+ url: verifyOptions.verifyHref,
+ recipient: verifyOptions.to,
+ lang: ctx.req.getLocale()
+ };
+
+ const email = new Email('email-verify', params);
+ email.send();
+
+ cb(null, verifyOptions.to);
+ }
+ }
+
+ const options = {
+ type: 'email',
+ to: instance.email,
+ from: {},
+ redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
+ template: false,
+ mailer: new Mailer,
+ host: url[1].split('/')[2],
+ port: url[2],
+ protocol: url[0],
+ user: Self
+ };
+
+ await user.verify(options);
+ });
+
Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data',
accepts: [
{
arg: 'ctx',
- type: 'Object',
+ type: 'object',
http: {source: 'context'}
}
],
returns: {
- type: 'Object',
+ type: 'object',
root: true
},
http: {
@@ -58,7 +107,7 @@ module.exports = Self => {
*
* @param {Integer} userId The user id
* @param {String} name The role name
- * @param {Object} options Options
+ * @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise
*/
Self.hasRole = async function(userId, name, options) {
@@ -70,8 +119,8 @@ module.exports = Self => {
* Get all user roles.
*
* @param {Integer} userId The user id
- * @param {Object} options Options
- * @return {Object} User role list
+ * @param {object} options Options
+ * @return {object} User role list
*/
Self.getRoles = async(userId, options) => {
let result = await Self.rawSql(
diff --git a/back/models/account.json b/back/models/account.json
index d0c17e70f..5e35c711a 100644
--- a/back/models/account.json
+++ b/back/models/account.json
@@ -40,6 +40,9 @@
"email": {
"type": "string"
},
+ "emailVerified": {
+ "type": "boolean"
+ },
"created": {
"type": "date"
},
@@ -88,16 +91,23 @@
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
- },
+ },
+ {
+ "property": "recoverPassword",
+ "accessType": "EXECUTE",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ },
{
- "property": "logout",
+ "property": "logout",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
- "property": "validateToken",
+ "property": "validateToken",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
diff --git a/back/models/chat.js b/back/models/chat.js
index f45d15180..a18edbd3f 100644
--- a/back/models/chat.js
+++ b/back/models/chat.js
@@ -11,9 +11,11 @@ module.exports = Self => {
let {message} = ctx.instance;
if (!message) return;
- const parts = message.match(/(?<=\[).*(?=])/g);
+ const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
+ if (!parts) return;
+
const replacedParts = parts.map(part => {
- return part.replace(/[*()]/g, '');
+ return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
});
for (const [index, part] of parts.entries())
diff --git a/back/models/specs/account.spec.js b/back/models/specs/account.spec.js
index c52bc4378..f31c81b75 100644
--- a/back/models/specs/account.spec.js
+++ b/back/models/specs/account.spec.js
@@ -1,14 +1,14 @@
-const app = require('vn-loopback/server/server');
+const models = require('vn-loopback/server/server').models;
describe('loopback model Account', () => {
it('should return true if the user has the given role', async() => {
- let result = await app.models.Account.hasRole(1, 'employee');
+ let result = await models.Account.hasRole(1, 'employee');
expect(result).toBeTruthy();
});
it('should return false if the user doesnt have the given role', async() => {
- let result = await app.models.Account.hasRole(1, 'administrator');
+ let result = await models.Account.hasRole(1, 'administrator');
expect(result).toBeFalsy();
});
diff --git a/back/models/specs/user.spec.js b/back/models/specs/user.spec.js
new file mode 100644
index 000000000..124afdc0c
--- /dev/null
+++ b/back/models/specs/user.spec.js
@@ -0,0 +1,32 @@
+const models = require('vn-loopback/server/server').models;
+const LoopBackContext = require('loopback-context');
+
+describe('account recoverPassword()', () => {
+ const userId = 1107;
+
+ const activeCtx = {
+ accessToken: {userId: userId},
+ http: {
+ req: {
+ headers: {origin: 'http://localhost'}
+ }
+ }
+ };
+
+ beforeEach(() => {
+ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
+ active: activeCtx
+ });
+ });
+
+ it('should send email with token', async() => {
+ const userId = 1107;
+ const user = await models.Account.findById(userId);
+
+ await models.Account.recoverPassword(user.email);
+
+ const result = await models.AccessToken.findOne({where: {userId: userId}});
+
+ expect(result).toBeDefined();
+ });
+});
diff --git a/back/models/user.js b/back/models/user.js
new file mode 100644
index 000000000..b24d702b3
--- /dev/null
+++ b/back/models/user.js
@@ -0,0 +1,27 @@
+const LoopBackContext = require('loopback-context');
+const {Email} = require('vn-print');
+
+module.exports = function(Self) {
+ Self.on('resetPasswordRequest', async function(info) {
+ const loopBackContext = LoopBackContext.getCurrentContext();
+ const httpCtx = {req: loopBackContext.active};
+ const httpRequest = httpCtx.req.http.req;
+ const headers = httpRequest.headers;
+ const origin = headers.origin;
+
+ const user = await Self.app.models.Account.findById(info.user.id);
+ const params = {
+ recipient: info.email,
+ lang: user.lang,
+ url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}`
+ };
+
+ const options = Object.assign({}, info.options);
+ for (const param in options)
+ params[param] = options[param];
+
+ const email = new Email(options.emailTemplate, params);
+
+ return email.send();
+ });
+};
diff --git a/db/changes/10502-november/00-aclUserPassword.sql b/db/changes/10502-november/00-aclUserPassword.sql
new file mode 100644
index 000000000..b92b54c28
--- /dev/null
+++ b/db/changes/10502-november/00-aclUserPassword.sql
@@ -0,0 +1,2 @@
+DELETE FROM `salix`.`ACL`
+ WHERE model = 'UserPassword';
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index dd037bfae..8e2ecc809 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -2564,10 +2564,6 @@ UPDATE `vn`.`route`
UPDATE `vn`.`route`
SET `invoiceInFk`=2
WHERE `id`=2;
-INSERT INTO `bs`.`salesPerson` (`workerFk`, `year`, `month`, `portfolioWeight`)
- VALUES
- (18, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 807.23),
- (19, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 34.40);
INSERT INTO `bs`.`sale` (`saleFk`, `amount`, `dated`, `typeFk`, `clientFk`)
VALUES
diff --git a/db/dump/structure.sql b/db/dump/structure.sql
index 6d85d0511..403534787 100644
--- a/db/dump/structure.sql
+++ b/db/dump/structure.sql
@@ -81044,4 +81044,3 @@ USE `vncontrol`;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-09-16 10:44:31
-
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index 06fabe3e6..f550e3a9d 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -29,6 +29,11 @@ export default {
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
},
+ recoverPassword: {
+ recoverPasswordButton: 'vn-login a[ui-sref="recoverPassword"]',
+ email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]',
+ sendEmailButton: 'vn-recover-password vn-submit',
+ },
accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',
diff --git a/e2e/paths/01-salix/04_recoverPassword.spec.js b/e2e/paths/01-salix/04_recoverPassword.spec.js
new file mode 100644
index 000000000..80ef32cb5
--- /dev/null
+++ b/e2e/paths/01-salix/04_recoverPassword.spec.js
@@ -0,0 +1,40 @@
+import selectors from '../../helpers/selectors';
+import getBrowser from '../../helpers/puppeteer';
+
+describe('Login path', async() => {
+ let browser;
+ let page;
+
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+
+ await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
+ await page.waitForState('recoverPassword');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it('should not throw error if not exist user', async() => {
+ await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com');
+ await page.waitToClick(selectors.recoverPassword.sendEmailButton);
+
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain('Notification sent!');
+ });
+
+ it('should send email', async() => {
+ await page.waitForState('login');
+ await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
+
+ await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com');
+ await page.waitToClick(selectors.recoverPassword.sendEmailButton);
+ const message = await page.waitForSnackbar();
+ await page.waitForState('login');
+
+ expect(message.text).toContain('Notification sent!');
+ });
+});
diff --git a/e2e/paths/05-ticket/20_future.spec.js b/e2e/paths/05-ticket/20_future.spec.js
index 4fee9523b..6db2bf4f0 100644
--- a/e2e/paths/05-ticket/20_future.spec.js
+++ b/e2e/paths/05-ticket/20_future.spec.js
@@ -5,80 +5,78 @@ describe('Ticket Future path', () => {
let browser;
let page;
- beforeAll(async () => {
+ beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'ticket');
await page.accessToSection('ticket.future');
});
- afterAll(async () => {
+ afterAll(async() => {
await browser.close();
});
const now = new Date();
const tomorrow = new Date(now.getDate() + 1);
- const ticket = {
- originDated: now,
- futureDated: now,
- linesMax: '9999',
- litersMax: '9999',
- warehouseFk: 'Warehouse One'
- };
- it('should show errors snackbar because of the required data', async () => {
+ it('should show errors snackbar because of the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit);
let message = await page.waitForSnackbar();
+
expect(message.text).toContain('warehouseFk is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.litersMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
+
expect(message.text).toContain('litersMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.linesMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
+
expect(message.text).toContain('linesMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
+
expect(message.text).toContain('futureDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.originDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
+
expect(message.text).toContain('originDated is a required argument');
});
- it('should search with the required data', async () => {
+ it('should search with the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search with the origin shipped today', async () => {
+ it('should search with the origin shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, now);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search with the origin shipped tomorrow', async () => {
+ it('should search with the origin shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
- it('should search with the destination shipped today', async () => {
+ it('should search with the destination shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.pickDate(selectors.ticketFuture.tfShipped, now);
@@ -86,14 +84,14 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search with the destination shipped tomorrow', async () => {
+ it('should search with the destination shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
- it('should search with the origin IPT', async () => {
+ it('should search with the origin IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@@ -108,7 +106,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
- it('should search with the destination IPT', async () => {
+ it('should search with the destination IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@@ -123,7 +121,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
- it('should search with the origin grouped state', async () => {
+ it('should search with the origin grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@@ -138,7 +136,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 3);
});
- it('should search with the destination grouped state', async () => {
+ it('should search with the destination grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
@@ -164,10 +162,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with an ID Origin', async () => {
+ it('should search in smart-table with an ID Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableId, "13");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableId, '13');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -176,10 +174,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with an ID Destination', async () => {
+ it('should search in smart-table with an ID Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableTfId, "12");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableTfId, '12');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -188,7 +186,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with an IPT Origin', async () => {
+ it('should search in smart-table with an IPT Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@@ -199,7 +197,7 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with an IPT Destination', async () => {
+ it('should search in smart-table with an IPT Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
@@ -210,10 +208,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with especified Lines', async () => {
+ it('should search in smart-table with especified Lines', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableLines, "0");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableLines, '0');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -222,8 +220,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableLines, "1");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableLines, '1');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -232,10 +230,10 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should search in smart-table with especified Liters', async () => {
+ it('should search in smart-table with especified Liters', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableLiters, "0");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableLiters, '0');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -244,8 +242,8 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
- await page.write(selectors.ticketFuture.tableLiters, "28");
- await page.keyboard.press("Enter");
+ await page.write(selectors.ticketFuture.tableLiters, '28');
+ await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
@@ -254,13 +252,13 @@ describe('Ticket Future path', () => {
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
- it('should check the three last tickets and move to the future', async () => {
+ it('should check the three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton);
const message = await page.waitForSnackbar();
+
expect(message.text).toContain('Tickets moved successfully!');
});
-
});
diff --git a/front/core/lib/component.js b/front/core/lib/component.js
index f17db68a2..5695d9449 100644
--- a/front/core/lib/component.js
+++ b/front/core/lib/component.js
@@ -12,9 +12,10 @@ export default class Component extends EventEmitter {
* @param {HTMLElement} $element The main component element
* @param {$rootScope.Scope} $scope The element scope
* @param {Function} $transclude The transclusion function
+ * @param {Function} $location The location function
*/
- constructor($element, $scope, $transclude) {
- super();
+ constructor($element, $scope, $transclude, $location) {
+ super($element, $scope, $transclude, $location);
this.$ = $scope;
if (!$element) return;
@@ -164,7 +165,7 @@ export default class Component extends EventEmitter {
$transclude.$$boundTransclude.$$slots[slot];
}
}
-Component.$inject = ['$element', '$scope'];
+Component.$inject = ['$element', '$scope', '$location', '$state'];
/*
* Automatically adds the most used services to the prototype, so they are
diff --git a/front/core/services/auth.js b/front/core/services/auth.js
index a1dcfa395..04520cd0b 100644
--- a/front/core/services/auth.js
+++ b/front/core/services/auth.js
@@ -23,7 +23,10 @@ export default class Auth {
initialize() {
let criteria = {
- to: state => state.name != 'login'
+ to: state => {
+ const outLayout = ['login', 'recoverPassword', 'resetPassword'];
+ return !outLayout.some(ol => ol == state.name);
+ }
};
this.$transitions.onStart(criteria, transition => {
if (this.loggedIn)
diff --git a/front/salix/components/app/app.html b/front/salix/components/app/app.html
index d32c9f68b..f14fab2dd 100644
--- a/front/salix/components/app/app.html
+++ b/front/salix/components/app/app.html
@@ -1,9 +1,8 @@
-
-
+
diff --git a/front/salix/components/app/app.js b/front/salix/components/app/app.js
index 1f8cdb46e..20f0ad969 100644
--- a/front/salix/components/app/app.js
+++ b/front/salix/components/app/app.js
@@ -9,13 +9,20 @@ import Component from 'core/lib/component';
* @property {SideMenu} rightMenu The left menu, if it's present
*/
export default class App extends Component {
+ constructor($element, $, $location, $state) {
+ super($element, $, $location, $state);
+ this.$location = $location;
+ this.$state = $state;
+ }
+
$postLink() {
this.vnApp.logger = this;
}
get showLayout() {
- let state = this.$state.current.name;
- return state && state != 'login';
+ const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
+ const outLayout = ['login', 'recoverPassword', 'resetPassword'];
+ return state && !outLayout.some(ol => ol == state);
}
$onDestroy() {
diff --git a/front/salix/components/index.js b/front/salix/components/index.js
index ce4ad585a..dbe9fe81a 100644
--- a/front/salix/components/index.js
+++ b/front/salix/components/index.js
@@ -5,7 +5,10 @@ import './descriptor-popover';
import './home/home';
import './layout';
import './left-menu/left-menu';
+import './login/index';
import './login/login';
+import './login/recover-password';
+import './login/reset-password';
import './module-card';
import './module-main';
import './side-menu/side-menu';
diff --git a/front/salix/components/log/index.js b/front/salix/components/log/index.js
index 2796931a0..f30878b9f 100644
--- a/front/salix/components/log/index.js
+++ b/front/salix/components/log/index.js
@@ -37,7 +37,7 @@ export default class Controller extends Section {
const validations = window.validations;
value.forEach(log => {
- const locale = validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
+ const locale = validations[log.changedModel] && validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
log.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale);
diff --git a/front/salix/components/login/index.html b/front/salix/components/login/index.html
new file mode 100644
index 000000000..186979f8c
--- /dev/null
+++ b/front/salix/components/login/index.html
@@ -0,0 +1,6 @@
+
-
-
+
+
+
+
+
+
+
diff --git a/front/salix/components/login/recover-password.html b/front/salix/components/login/recover-password.html
new file mode 100644
index 000000000..73f5401d9
--- /dev/null
+++ b/front/salix/components/login/recover-password.html
@@ -0,0 +1,17 @@
+
Recover password
+
+
+
+ We will sent you an email to recover your password
+
+
diff --git a/front/salix/components/login/recover-password.js b/front/salix/components/login/recover-password.js
new file mode 100644
index 000000000..fa9bfc459
--- /dev/null
+++ b/front/salix/components/login/recover-password.js
@@ -0,0 +1,37 @@
+import ngModule from '../../module';
+import './style.scss';
+
+export default class Controller {
+ constructor($scope, $element, $http, vnApp, $translate, $state) {
+ Object.assign(this, {
+ $scope,
+ $element,
+ $http,
+ vnApp,
+ $translate,
+ $state
+ });
+ }
+
+ goToLogin() {
+ this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
+ this.$state.go('login');
+ }
+
+ submit() {
+ const params = {
+ email: this.email
+ };
+
+ this.$http.post('Accounts/recoverPassword', params)
+ .then(() => {
+ this.goToLogin();
+ });
+ }
+}
+Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state'];
+
+ngModule.vnComponent('vnRecoverPassword', {
+ template: require('./recover-password.html'),
+ controller: Controller
+});
diff --git a/front/salix/components/login/reset-password.html b/front/salix/components/login/reset-password.html
new file mode 100644
index 000000000..bdbdc113e
--- /dev/null
+++ b/front/salix/components/login/reset-password.html
@@ -0,0 +1,19 @@
+
Reset password
+
+
+
+
+
diff --git a/front/salix/components/login/reset-password.js b/front/salix/components/login/reset-password.js
new file mode 100644
index 000000000..9ee1fdb62
--- /dev/null
+++ b/front/salix/components/login/reset-password.js
@@ -0,0 +1,48 @@
+import ngModule from '../../module';
+import './style.scss';
+
+export default class Controller {
+ constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
+ Object.assign(this, {
+ $scope,
+ $element,
+ $http,
+ vnApp,
+ $translate,
+ $state,
+ $location
+ });
+ }
+
+ $onInit() {
+ this.$http.get('UserPasswords/findOne')
+ .then(res => {
+ this.passRequirements = res.data;
+ });
+ }
+
+ submit() {
+ if (!this.newPassword)
+ throw new UserError(`You must enter a new password`);
+ if (this.newPassword != this.repeatPassword)
+ throw new UserError(`Passwords don't match`);
+
+ const headers = {
+ Authorization: this.$location.$$search.access_token
+ };
+
+ const newPassword = this.newPassword;
+
+ this.$http.post('users/reset-password', {newPassword}, {headers})
+ .then(() => {
+ this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
+ this.$state.go('login');
+ });
+ }
+}
+Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
+
+ngModule.vnComponent('vnResetPassword', {
+ template: require('./reset-password.html'),
+ controller: Controller
+});
diff --git a/front/salix/components/login/style.scss b/front/salix/components/login/style.scss
index 8ebf2a68c..8985893f2 100644
--- a/front/salix/components/login/style.scss
+++ b/front/salix/components/login/style.scss
@@ -1,6 +1,31 @@
@import "variables";
-vn-login {
+vn-login,
+vn-reset-password,
+vn-recover-password{
+ .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;
+ }
+ }
+}
+
+vn-out-layout{
position: absolute;
height: 100%;
width: 100%;
@@ -39,28 +64,17 @@ vn-login {
white-space: inherit;
}
}
- & > .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;
- }
- }
}
+
+ h5{
+ color: $color-primary;
+ }
+
+ .text-secondary{
+ text-align: center;
+ padding-bottom: 16px;
+ }
+
}
@media screen and (max-width: 600px) {
@@ -71,4 +85,8 @@ vn-login {
box-shadow: none;
}
}
+
+ a{
+ color: $color-primary;
+ }
}
diff --git a/front/salix/module.js b/front/salix/module.js
index a8de61ae0..01df01a67 100644
--- a/front/salix/module.js
+++ b/front/salix/module.js
@@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) {
switch (exception.status) {
case 401:
- if ($state.current.name != 'login') {
+ if (!$state.current.name.includes('login')) {
messageT = 'Session has expired';
let params = {continue: $window.location.hash};
$state.go('login', params);
diff --git a/front/salix/routes.js b/front/salix/routes.js
index 600907ff1..be893800f 100644
--- a/front/salix/routes.js
+++ b/front/salix/routes.js
@@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
.state('login', {
url: '/login?continue',
description: 'Login',
- views: {
- login: {template: '
'}
- }
+ template: '
'
+ })
+ .state('recoverPassword', {
+ url: '/recover-password',
+ description: 'Recover-password',
+ template: '
asd'
+ })
+ .state('resetPassword', {
+ url: '/reset-password',
+ description: 'Reset-password',
+ template: '
'
})
.state('home', {
url: '/',
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index c26941a20..cbff8d75f 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -131,12 +131,15 @@
"Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
+ "Verify email": "Verify email",
+ "Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it",
"Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
+ "Email verify": "Email verify",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"App locked": "App locked by user {{userId}}",
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index b5ae7bd49..6bf4d79e3 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -246,6 +246,8 @@
"There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio",
"App locked": "Aplicación bloqueada por el usuario {{userId}}",
+ "Email verify": "Correo de verificación",
+ "Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment",
"Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente"
diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json
index 4db642058..00f6bf624 100644
--- a/loopback/server/datasources.json
+++ b/loopback/server/datasources.json
@@ -113,4 +113,4 @@
"application/x-7z-compressed"
]
}
-}
\ No newline at end of file
+}
diff --git a/modules/account/back/models/user-password.json b/modules/account/back/models/user-password.json
index 1b7e49edd..53909ad1f 100644
--- a/modules/account/back/models/user-password.json
+++ b/modules/account/back/models/user-password.json
@@ -30,5 +30,13 @@
"type": "number",
"required": true
}
- }
+ },
+ "acls": [
+ {
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ }
+ ]
}
diff --git a/modules/account/front/basic-data/index.js b/modules/account/front/basic-data/index.js
index 342297e45..77d3eab26 100644
--- a/modules/account/front/basic-data/index.js
+++ b/modules/account/front/basic-data/index.js
@@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
+ $onInit() {
+ if (this.$params.emailConfirmed)
+ this.vnApp.showSuccess(this.$t('Email verified successfully!'));
+ }
+
onSubmit() {
this.$.watcher.submit()
.then(() => this.card.reload());
diff --git a/modules/account/front/basic-data/locale/es.yml b/modules/account/front/basic-data/locale/es.yml
new file mode 100644
index 000000000..2ca7bf698
--- /dev/null
+++ b/modules/account/front/basic-data/locale/es.yml
@@ -0,0 +1 @@
+Email verified successfully!: Correo verificado correctamente!
diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json
index b96c931c9..a6f2f5d3f 100644
--- a/modules/account/front/routes.json
+++ b/modules/account/front/routes.json
@@ -74,7 +74,7 @@
}
},
{
- "url": "/basic-data",
+ "url": "/basic-data?emailConfirmed",
"state": "account.card.basicData",
"component": "vn-user-basic-data",
"description": "Basic data",
diff --git a/modules/client/back/methods/client/filter.js b/modules/client/back/methods/client/filter.js
index 3e1ea43bb..1ae569fd3 100644
--- a/modules/client/back/methods/client/filter.js
+++ b/modules/client/back/methods/client/filter.js
@@ -91,7 +91,18 @@ module.exports = Self => {
case 'search':
return /^\d+$/.test(value)
? {'c.id': {inq: value}}
- : {'c.name': {like: `%${value}%`}};
+ : {or: [
+ {'c.name': {like: `%${value}%`}},
+ {'c.socialName': {like: `%${value}%`}},
+ ]};
+ case 'phone':
+ return {or: [
+ {'c.phone': {like: `%${value}%`}},
+ {'c.mobile': {like: `%${value}%`}},
+ ]};
+ case 'zoneFk':
+ param = 'a.postalCode';
+ return {[param]: {inq: postalCode}};
case 'name':
case 'salesPersonFk':
case 'fi':
@@ -100,12 +111,8 @@ module.exports = Self => {
case 'postcode':
case 'provinceFk':
case 'email':
- case 'phone':
param = `c.${param}`;
- return {[param]: value};
- case 'zoneFk':
- param = 'a.postalCode';
- return {[param]: {inq: postalCode}};
+ return {[param]: {like: `%${value}%`}};
}
});
@@ -119,6 +126,7 @@ module.exports = Self => {
c.fi,
c.socialName,
c.phone,
+ c.mobile,
c.city,
c.postcode,
c.email,
@@ -132,7 +140,7 @@ module.exports = Self => {
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN province p ON p.id = c.provinceFk
JOIN vn.address a ON a.clientFk = c.id
- `
+ `
);
stmt.merge(conn.makeWhere(filter.where));
diff --git a/modules/client/back/methods/client/specs/updatePortfolio.spec.js b/modules/client/back/methods/client/specs/updatePortfolio.spec.js
deleted file mode 100644
index bf681eb2e..000000000
--- a/modules/client/back/methods/client/specs/updatePortfolio.spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const models = require('vn-loopback/server/server').models;
-const LoopBackContext = require('loopback-context');
-
-describe('Client updatePortfolio', () => {
- const activeCtx = {
- accessToken: {userId: 9},
- http: {
- req: {
- headers: {origin: 'http://localhost'},
- [`__`]: value => {
- return value;
- }
- }
- }
- };
-
- beforeAll(() => {
- spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
- active: activeCtx
- });
- });
-
- it('should update the portfolioWeight when the salesPerson of a client changes', async() => {
- const clientId = 1108;
- const salesPersonId = 18;
-
- const tx = await models.Client.beginTransaction({});
-
- try {
- const options = {transaction: tx};
- const expectedResult = 841.63;
-
- const client = await models.Client.findById(clientId, null, options);
- await client.updateAttribute('salesPersonFk', salesPersonId, options);
-
- await models.Client.updatePortfolio(options);
-
- const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `;
- const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options);
-
- expect(salesPerson.portfolioWeight).toEqual(expectedResult);
-
- await tx.rollback();
- } catch (e) {
- await tx.rollback();
- throw e;
- }
- });
-
- it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => {
- const clientId = 1107;
- const salesPersonId = 19;
- const tx = await models.Client.beginTransaction({});
-
- try {
- const options = {transaction: tx};
- const expectedResult = 34.40;
-
- const client = await models.Client.findById(clientId, null, options);
- await client.updateAttribute('salesPersonFk', null, options);
-
- await models.Client.updatePortfolio();
-
- const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `;
- const [salesPerson] = await models.Client.rawSql(portfolioQuery);
-
- expect(salesPerson.portfolioWeight).toEqual(expectedResult);
-
- await tx.rollback();
- } catch (e) {
- await tx.rollback();
- throw e;
- }
- });
-});
diff --git a/modules/client/back/methods/client/updatePortfolio.js b/modules/client/back/methods/client/updatePortfolio.js
deleted file mode 100644
index 809a84636..000000000
--- a/modules/client/back/methods/client/updatePortfolio.js
+++ /dev/null
@@ -1,25 +0,0 @@
-module.exports = function(Self) {
- Self.remoteMethodCtx('updatePortfolio', {
- description: 'Update salesPeson potfolio weight',
- accessType: 'READ',
- accepts: [],
- returns: {
- type: 'Object',
- root: true
- },
- http: {
- path: `/updatePortfolio`,
- verb: 'GET'
- }
- });
-
- Self.updatePortfolio = async options => {
- const myOptions = {};
-
- if (typeof options == 'object')
- Object.assign(myOptions, options);
-
- query = `CALL bs.salesPerson_updatePortfolio()`;
- return Self.rawSql(query, null, myOptions);
- };
-};
diff --git a/modules/client/back/models/client-methods.js b/modules/client/back/models/client-methods.js
index 5134e3942..4b20a822c 100644
--- a/modules/client/back/models/client-methods.js
+++ b/modules/client/back/models/client-methods.js
@@ -22,7 +22,6 @@ module.exports = Self => {
require('../methods/client/summary')(Self);
require('../methods/client/updateAddress')(Self);
require('../methods/client/updateFiscalData')(Self);
- require('../methods/client/updatePortfolio')(Self);
require('../methods/client/updateUser')(Self);
require('../methods/client/uploadFile')(Self);
require('../methods/client/campaignMetricsPdf')(Self);
diff --git a/modules/client/front/basic-data/index.js b/modules/client/front/basic-data/index.js
index 418663952..b08d642d1 100644
--- a/modules/client/front/basic-data/index.js
+++ b/modules/client/front/basic-data/index.js
@@ -10,8 +10,6 @@ export default class Controller extends Section {
onSubmit() {
return this.$.watcher.submit().then(() => {
- const query = `Clients/updatePortfolio`;
- this.$http.get(query);
this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`);
});
}
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
index 803338ef3..ee3310368 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js
@@ -11,6 +11,7 @@ describe('InvoiceOut createPdf()', () => {
const ctx = {req: activeCtx};
it('should create a new PDF file and set true the hasPdf property', async() => {
+ pending('https://redmine.verdnatura.es/issues/4875');
const invoiceId = 1;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
index 08f049783..536fa07a0 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js
@@ -30,6 +30,7 @@ describe('InvoiceOut downloadZip()', () => {
});
it('should return an error if the size of the files is too large', async() => {
+ pending('https://redmine.verdnatura.es/issues/4875');
const tx = await models.InvoiceOut.beginTransaction({});
let error;
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js
index 02f982011..7b5886236 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/filter.spec.js
@@ -51,6 +51,7 @@ describe('InvoiceOut filter()', () => {
});
it('should return the invoice out matching hasPdf', async() => {
+ pending('https://redmine.verdnatura.es/issues/4875');
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
diff --git a/modules/route/back/methods/route/getTickets.js b/modules/route/back/methods/route/getTickets.js
index 18e5abaf2..708644c1a 100644
--- a/modules/route/back/methods/route/getTickets.js
+++ b/modules/route/back/methods/route/getTickets.js
@@ -33,36 +33,36 @@ module.exports = Self => {
const stmt = new ParameterizedSQL(
`SELECT
- t.id,
- t.packages,
- t.warehouseFk,
- t.nickname,
- t.clientFk,
- t.priority,
- t.addressFk,
- st.code AS ticketStateCode,
- st.name AS ticketStateName,
- wh.name AS warehouseName,
- tob.description AS ticketObservation,
- a.street,
- a.postalCode,
- a.city,
- am.name AS agencyModeName,
- u.nickname AS userNickname,
- vn.ticketTotalVolume(t.id) AS volume,
- tob.description
- FROM route r
- JOIN ticket t ON t.routeFk = r.id
- LEFT JOIN ticketState ts ON ts.ticketFk = t.id
- LEFT JOIN state st ON st.id = ts.stateFk
- LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
- LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
- LEFT JOIN observationType ot ON tob.observationTypeFk = ot.id
- AND ot.code = 'delivery'
- LEFT JOIN address a ON a.id = t.addressFk
- LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
- LEFT JOIN account.user u ON u.id = r.workerFk
- LEFT JOIN vehicle v ON v.id = r.vehicleFk`
+ t.id,
+ t.packages,
+ t.warehouseFk,
+ t.nickname,
+ t.clientFk,
+ t.priority,
+ t.addressFk,
+ st.code AS ticketStateCode,
+ st.name AS ticketStateName,
+ wh.name AS warehouseName,
+ tob.description AS ticketObservation,
+ a.street,
+ a.postalCode,
+ a.city,
+ am.name AS agencyModeName,
+ u.nickname AS userNickname,
+ vn.ticketTotalVolume(t.id) AS volume,
+ tob.description
+ FROM vn.route r
+ JOIN ticket t ON t.routeFk = r.id
+ LEFT JOIN ticketState ts ON ts.ticketFk = t.id
+ LEFT JOIN state st ON st.id = ts.stateFk
+ LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
+ LEFT JOIN observationType ot ON ot.code = 'delivery'
+ LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
+ AND tob.observationTypeFk = ot.id
+ LEFT JOIN address a ON a.id = t.addressFk
+ LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
+ LEFT JOIN account.user u ON u.id = r.workerFk
+ LEFT JOIN vehicle v ON v.id = r.vehicleFk`
);
if (!filter.where) filter.where = {};
diff --git a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js
new file mode 100644
index 000000000..7825f38b8
--- /dev/null
+++ b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js
@@ -0,0 +1,181 @@
+const Imap = require('imap');
+module.exports = Self => {
+ Self.remoteMethod('checkInbox', {
+ description: 'Check an email inbox and process it',
+ accessType: 'READ',
+ returns:
+ {
+ arg: 'body',
+ type: 'file',
+ root: true
+ },
+ http: {
+ path: `/checkInbox`,
+ verb: 'POST'
+ }
+ });
+
+ Self.checkInbox = async() => {
+ let imapConfig = await Self.app.models.WorkerTimeControlParams.findOne();
+ let imap = new Imap({
+ user: imapConfig.mailUser,
+ password: imapConfig.mailPass,
+ host: imapConfig.mailHost,
+ port: 993,
+ tls: true
+ });
+ let isEmailOk;
+ let uid;
+ let emailBody;
+
+ function openInbox(cb) {
+ imap.openBox('INBOX', true, cb);
+ }
+
+ imap.once('ready', function() {
+ openInbox(function(err, box) {
+ if (err) throw err;
+ const totalMessages = box.messages.total;
+ if (totalMessages == 0)
+ imap.end();
+
+ let f = imap.seq.fetch('1:*', {
+ bodies: ['HEADER.FIELDS (FROM SUBJECT)', '1'],
+ struct: true
+ });
+ f.on('message', function(msg, seqno) {
+ isEmailOk = false;
+ msg.on('body', function(stream, info) {
+ let buffer = '';
+ let bufferCopy = '';
+ stream.on('data', function(chunk) {
+ buffer = chunk.toString('utf8');
+ if (info.which === '1' && bufferCopy.length == 0)
+ bufferCopy = buffer.replace(/\s/g, ' ');
+ });
+ stream.on('end', function() {
+ if (bufferCopy.length > 0) {
+ emailBody = bufferCopy.toUpperCase().trim();
+
+ const bodyPositionOK = emailBody.match(/\bOK\b/i);
+ if (bodyPositionOK != null && (bodyPositionOK.index == 0 || bodyPositionOK.index == 122))
+ isEmailOk = true;
+ else
+ isEmailOk = false;
+ }
+ });
+ msg.once('attributes', function(attrs) {
+ uid = attrs.uid;
+ });
+ msg.once('end', function() {
+ if (info.which === 'HEADER.FIELDS (FROM SUBJECT)') {
+ if (isEmailOk) {
+ imap.move(uid, 'exito', function(err) {
+ });
+ emailConfirm(buffer);
+ } else {
+ imap.move(uid, 'error', function(err) {
+ });
+ emailReply(buffer, emailBody);
+ }
+ }
+ });
+ });
+ });
+ f.once('end', function() {
+ imap.end();
+ });
+ });
+ });
+
+ imap.connect();
+ return 'Leer emails de gestion horaria';
+ };
+
+ async function emailConfirm(buffer) {
+ const now = new Date();
+ const from = JSON.stringify(Imap.parseHeader(buffer).from);
+ const subject = JSON.stringify(Imap.parseHeader(buffer).subject);
+
+ const timeControlDate = await getEmailDate(subject);
+ const week = timeControlDate[0];
+ const year = timeControlDate[1];
+ const user = await getUser(from);
+ let workerMail;
+
+ if (user.id != null) {
+ workerMail = await Self.app.models.WorkerTimeControlMail.findOne({
+ where: {
+ week: week,
+ year: year,
+ workerFk: user.id
+ }
+ });
+ }
+ if (workerMail != null) {
+ await workerMail.updateAttributes({
+ updated: now,
+ state: 'CONFIRMED'
+ });
+ }
+ }
+
+ async function emailReply(buffer, emailBody) {
+ const now = new Date();
+ const from = JSON.stringify(Imap.parseHeader(buffer).from);
+ const subject = JSON.stringify(Imap.parseHeader(buffer).subject);
+
+ const timeControlDate = await getEmailDate(subject);
+ const week = timeControlDate[0];
+ const year = timeControlDate[1];
+ const user = await getUser(from);
+ let workerMail;
+
+ if (user.id != null) {
+ workerMail = await Self.app.models.WorkerTimeControlMail.findOne({
+ where: {
+ week: week,
+ year: year,
+ workerFk: user.id
+ }
+ });
+
+ if (workerMail != null) {
+ await workerMail.updateAttributes({
+ updated: now,
+ state: 'REVISE',
+ reason: emailBody
+ });
+ } else
+ await sendMail(user, subject, emailBody);
+ }
+ }
+
+ async function getUser(workerEmail) {
+ const userEmail = workerEmail.match(/(?<=<)(.*?)(?=>)/);
+
+ let [user] = await Self.rawSql(`SELECT u.id,u.name FROM account.user u
+ LEFT JOIN account.mailForward m on m.account = u.id
+ WHERE forwardTo =? OR
+ CONCAT(u.name,'@verdnatura.es') = ?`,
+ [userEmail[0], userEmail[0]]);
+
+ return user;
+ }
+
+ async function getEmailDate(subject) {
+ const date = subject.match(/\d+/g);
+ return date;
+ }
+
+ async function sendMail(user, subject, emailBody) {
+ const sendTo = 'rrhh@verdnatura.es';
+ const emailSubject = subject + ' ' + user.name;
+
+ await Self.app.models.Mail.create({
+ receiver: sendTo,
+ subject: emailSubject,
+ body: emailBody
+ });
+ }
+};
diff --git a/modules/worker/back/methods/worker-time-control/sendMail.js b/modules/worker/back/methods/worker-time-control/sendMail.js
index 2f9559b3a..b38405c1d 100644
--- a/modules/worker/back/methods/worker-time-control/sendMail.js
+++ b/modules/worker/back/methods/worker-time-control/sendMail.js
@@ -133,7 +133,7 @@ module.exports = Self => {
tb.permissionRate,
d.isTeleworking
FROM tmp.timeBusinessCalculate tb
- JOIN user u ON u.id = tb.userFk
+ JOIN account.user u ON u.id = tb.userFk
JOIN department d ON d.id = tb.departmentFk
JOIN business b ON b.id = tb.businessFk
LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk AND tc.dated = tb.dated
@@ -143,7 +143,7 @@ module.exports = Self => {
IF(tc.timeWorkDecimal > 0, FALSE, IF(tb.timeWorkDecimal > 0, TRUE, FALSE)),
TRUE))isTeleworkingWeek
FROM tmp.timeBusinessCalculate tb
- LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk
+ LEFT JOIN tmp.timeControlCalculate tc ON tc.userFk = tb.userFk
AND tc.dated = tb.dated
GROUP BY tb.userFk
HAVING isTeleworkingWeek > 0
@@ -332,18 +332,9 @@ module.exports = Self => {
}, myOptions);
const timestamp = started.getTime() / 1000;
- await models.Mail.create({
- receiver: previousReceiver,
- subject: $t('Record of hours week', {
- week: args.week,
- year: args.year
- }),
- body: `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`
- }, myOptions);
+ const url = `${salix.url}worker/${previousWorkerFk}/time-control?timestamp=${timestamp}`;
- query = `INSERT IGNORE INTO workerTimeControlMail (workerFk, year, week)
- VALUES (?, ?, ?);`;
- await Self.rawSql(query, [previousWorkerFk, args.year, args.week], myOptions);
+ await models.WorkerTimeControl.weeklyHourRecordEmail(ctx, previousReceiver, args.week, args.year, url);
previousWorkerFk = day.workerFk;
previousReceiver = day.receiver;
diff --git a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js
index 4cc6e54e3..24bfd6904 100644
--- a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js
+++ b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js
@@ -2,15 +2,12 @@ const models = require('vn-loopback/server/server').models;
describe('workerTimeControl sendMail()', () => {
const workerId = 18;
- const ctx = {
- req: {
- __: value => {
- return value;
- }
- },
- args: {}
-
+ const activeCtx = {
+ getLocale: () => {
+ return 'en';
+ }
};
+ const ctx = {req: activeCtx, args: {}};
it('should fill time control of a worker without records in Journey and with rest', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
diff --git a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js
new file mode 100644
index 000000000..0cf614e57
--- /dev/null
+++ b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js
@@ -0,0 +1,53 @@
+const {Email} = require('vn-print');
+
+module.exports = Self => {
+ Self.remoteMethodCtx('weeklyHourRecordEmail', {
+ description: 'Sends the buyer waste email',
+ accessType: 'WRITE',
+ accepts: [
+ {
+ arg: 'recipient',
+ type: 'string',
+ description: 'The recipient email',
+ required: true,
+ },
+ {
+ arg: 'week',
+ type: 'number',
+ required: true,
+ },
+ {
+ arg: 'year',
+ type: 'number',
+ required: true
+ },
+ {
+ arg: 'url',
+ type: 'string',
+ required: true
+ }
+ ],
+ returns: {
+ type: ['object'],
+ root: true
+ },
+ http: {
+ path: '/weekly-hour-hecord-email',
+ verb: 'POST'
+ }
+ });
+
+ Self.weeklyHourRecordEmail = async(ctx, recipient, week, year, url) => {
+ const params = {
+ recipient: recipient,
+ lang: ctx.req.getLocale(),
+ week: week,
+ year: year,
+ url: url
+ };
+
+ const email = new Email('weekly-hour-record', params);
+
+ return email.send();
+ };
+};
diff --git a/modules/worker/back/models/worker-time-control-mail.js b/modules/worker/back/models/worker-time-control-mail.js
new file mode 100644
index 000000000..36f3851b6
--- /dev/null
+++ b/modules/worker/back/models/worker-time-control-mail.js
@@ -0,0 +1,3 @@
+module.exports = Self => {
+ require('../methods/worker-time-control-mail/checkInbox')(Self);
+};
diff --git a/modules/worker/back/models/worker-time-control.js b/modules/worker/back/models/worker-time-control.js
index 9f802511a..7339f5d15 100644
--- a/modules/worker/back/models/worker-time-control.js
+++ b/modules/worker/back/models/worker-time-control.js
@@ -7,6 +7,7 @@ module.exports = Self => {
require('../methods/worker-time-control/updateTimeEntry')(Self);
require('../methods/worker-time-control/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
+ require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
diff --git a/package-lock.json b/package-lock.json
index 07b0a95fd..2ed6cb275 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,7 +24,7 @@
"jsdom": "^16.7.0",
"jszip": "^3.10.0",
"ldapjs": "^2.2.0",
- "loopback": "^3.26.0",
+ "loopback": "^3.28.0",
"loopback-boot": "3.3.1",
"loopback-component-explorer": "^6.5.0",
"loopback-component-storage": "3.6.1",
@@ -4878,13 +4878,20 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001299",
+ "version": "1.0.30001434",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
+ "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"dev": true,
- "license": "CC-BY-4.0",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- }
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ]
},
"node_modules/canonical-json": {
"version": "0.0.4",
@@ -12881,6 +12888,66 @@
"xmlcreate": "^1.0.1"
}
},
+ "node_modules/jsbarcode": {
+ "version": "3.11.5",
+ "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
+ "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==",
+ "bin": {
+ "auto.js": "bin/barcodes/CODE128/auto.js",
+ "Barcode.js": "bin/barcodes/Barcode.js",
+ "barcodes": "bin/barcodes",
+ "canvas.js": "bin/renderers/canvas.js",
+ "checksums.js": "bin/barcodes/MSI/checksums.js",
+ "codabar": "bin/barcodes/codabar",
+ "CODE128": "bin/barcodes/CODE128",
+ "CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js",
+ "CODE128.js": "bin/barcodes/CODE128/CODE128.js",
+ "CODE128A.js": "bin/barcodes/CODE128/CODE128A.js",
+ "CODE128B.js": "bin/barcodes/CODE128/CODE128B.js",
+ "CODE128C.js": "bin/barcodes/CODE128/CODE128C.js",
+ "CODE39": "bin/barcodes/CODE39",
+ "constants.js": "bin/barcodes/ITF/constants.js",
+ "defaults.js": "bin/options/defaults.js",
+ "EAN_UPC": "bin/barcodes/EAN_UPC",
+ "EAN.js": "bin/barcodes/EAN_UPC/EAN.js",
+ "EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js",
+ "EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js",
+ "EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js",
+ "EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js",
+ "encoder.js": "bin/barcodes/EAN_UPC/encoder.js",
+ "ErrorHandler.js": "bin/exceptions/ErrorHandler.js",
+ "exceptions": "bin/exceptions",
+ "exceptions.js": "bin/exceptions/exceptions.js",
+ "fixOptions.js": "bin/help/fixOptions.js",
+ "GenericBarcode": "bin/barcodes/GenericBarcode",
+ "getOptionsFromElement.js": "bin/help/getOptionsFromElement.js",
+ "getRenderProperties.js": "bin/help/getRenderProperties.js",
+ "help": "bin/help",
+ "index.js": "bin/renderers/index.js",
+ "index.tmp.js": "bin/barcodes/index.tmp.js",
+ "ITF": "bin/barcodes/ITF",
+ "ITF.js": "bin/barcodes/ITF/ITF.js",
+ "ITF14.js": "bin/barcodes/ITF/ITF14.js",
+ "JsBarcode.js": "bin/JsBarcode.js",
+ "linearizeEncodings.js": "bin/help/linearizeEncodings.js",
+ "merge.js": "bin/help/merge.js",
+ "MSI": "bin/barcodes/MSI",
+ "MSI.js": "bin/barcodes/MSI/MSI.js",
+ "MSI10.js": "bin/barcodes/MSI/MSI10.js",
+ "MSI1010.js": "bin/barcodes/MSI/MSI1010.js",
+ "MSI11.js": "bin/barcodes/MSI/MSI11.js",
+ "MSI1110.js": "bin/barcodes/MSI/MSI1110.js",
+ "object.js": "bin/renderers/object.js",
+ "options": "bin/options",
+ "optionsFromStrings.js": "bin/help/optionsFromStrings.js",
+ "pharmacode": "bin/barcodes/pharmacode",
+ "renderers": "bin/renderers",
+ "shared.js": "bin/renderers/shared.js",
+ "svg.js": "bin/renderers/svg.js",
+ "UPC.js": "bin/barcodes/EAN_UPC/UPC.js",
+ "UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js"
+ }
+ },
"node_modules/jsbn": {
"version": "0.1.1",
"license": "MIT"
@@ -23759,6 +23826,14 @@
"version": "1.0.2",
"license": "Apache-2.0"
},
+ "node_modules/xmldom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
+ "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"license": "MIT",
@@ -23938,6 +24013,7 @@
"fs-extra": "^7.0.1",
"intl": "^1.2.5",
"js-yaml": "^3.13.1",
+ "jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0",
"juice": "^5.2.0",
"log4js": "^6.7.0",
@@ -23948,7 +24024,8 @@
"strftime": "^0.10.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.0",
- "vue-server-renderer": "^2.6.10"
+ "vue-server-renderer": "^2.6.10",
+ "xmldom": "^0.6.0"
}
},
"print/node_modules/fs-extra": {
@@ -28107,7 +28184,9 @@
"version": "1.0.0"
},
"caniuse-lite": {
- "version": "1.0.30001299",
+ "version": "1.0.30001434",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
+ "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==",
"dev": true
},
"canonical-json": {
@@ -39828,6 +39907,11 @@
"xmlcreate": "^1.0.1"
}
},
+ "jsbarcode": {
+ "version": "3.11.5",
+ "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz",
+ "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA=="
+ },
"jsbn": {
"version": "0.1.1"
},
@@ -57805,6 +57889,7 @@
"fs-extra": "^7.0.1",
"intl": "^1.2.5",
"js-yaml": "^3.13.1",
+ "jsbarcode": "^3.11.5",
"jsonexport": "^3.2.0",
"juice": "^5.2.0",
"log4js": "^6.7.0",
@@ -57815,7 +57900,8 @@
"strftime": "^0.10.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.0",
- "vue-server-renderer": "^2.6.10"
+ "vue-server-renderer": "^2.6.10",
+ "xmldom": "^0.6.0"
},
"dependencies": {
"fs-extra": {
@@ -59678,6 +59764,11 @@
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
"integrity": "sha512-Mbe56Dvj00onbnSo9J0qj/XlY5bfN9KidsOnpd5tRCsR3ekB3hyyNU9fGrTdqNT5ZNvv4BsA2TcQlignsZyVcw=="
},
+ "xmldom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
+ "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="
+ },
"xtend": {
<<<<<<< HEAD
"version": "1.0.3",
diff --git a/print/common/css/misc.css b/print/common/css/misc.css
index df8bf571a..ce6c641a0 100644
--- a/print/common/css/misc.css
+++ b/print/common/css/misc.css
@@ -49,4 +49,10 @@
.page-break-after {
page-break-after: always;
+}
+
+.ellipsize {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
}
\ No newline at end of file
diff --git a/print/core/smtp.js b/print/core/smtp.js
index a55ba448d..61b115b5a 100644
--- a/print/core/smtp.js
+++ b/print/core/smtp.js
@@ -8,10 +8,12 @@ module.exports = {
this.transporter = nodemailer.createTransport(config.smtp);
},
- send(options) {
+ async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
if (process.env.NODE_ENV !== 'production') {
+ const notProductionError = {message: 'This not production, this email not sended'};
+ await this.mailLog(options, notProductionError);
if (!config.smtp.auth.user)
return Promise.resolve(true);
@@ -24,29 +26,35 @@ module.exports = {
throw err;
}).finally(async() => {
- const attachments = [];
- if (options.attachments) {
- for (let attachment of options.attachments) {
- const fileName = attachment.filename;
- const filePath = attachment.path;
- if (fileName.includes('.png')) continue;
-
- if (fileName || filePath)
- attachments.push(filePath ? filePath : fileName);
- }
- }
-
- const fileNames = attachments.join(',\n');
- await db.rawSql(`
- INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
- VALUES (?, ?, 1, ?, ?, ?, ?)`, [
- options.to,
- options.replyTo,
- options.subject,
- options.text || options.html,
- fileNames,
- error && error.message || 'Sent'
- ]);
+ await this.mailLog(options, error);
});
+ },
+
+ async mailLog(options, error) {
+ const attachments = [];
+ if (options.attachments) {
+ for (let attachment of options.attachments) {
+ const fileName = attachment.filename;
+ const filePath = attachment.path;
+ if (fileName.includes('.png')) continue;
+
+ if (fileName || filePath)
+ attachments.push(filePath ? filePath : fileName);
+ }
+ }
+
+ const fileNames = attachments.join(',\n');
+
+ await db.rawSql(`
+ INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
+ VALUES (?, ?, 1, ?, ?, ?, ?)`, [
+ options.to,
+ options.replyTo,
+ options.subject,
+ options.text || options.html,
+ fileNames,
+ error && error.message || 'Sent'
+ ]);
}
+
};
diff --git a/print/templates/email/email-verify/assets/css/import.js b/print/templates/email/email-verify/assets/css/import.js
new file mode 100644
index 000000000..7360587f7
--- /dev/null
+++ b/print/templates/email/email-verify/assets/css/import.js
@@ -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();
+
diff --git a/print/templates/email/email-verify/assets/css/style.css b/print/templates/email/email-verify/assets/css/style.css
new file mode 100644
index 000000000..5db85befa
--- /dev/null
+++ b/print/templates/email/email-verify/assets/css/style.css
@@ -0,0 +1,5 @@
+.external-link {
+ border: 2px dashed #8dba25;
+ border-radius: 3px;
+ text-align: center
+}
\ No newline at end of file
diff --git a/print/templates/email/email-verify/email-verify.html b/print/templates/email/email-verify/email-verify.html
new file mode 100644
index 000000000..b8a6e263c
--- /dev/null
+++ b/print/templates/email/email-verify/email-verify.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
{{ $t('subject') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
diff --git a/print/templates/email/email-verify/email-verify.js b/print/templates/email/email-verify/email-verify.js
new file mode 100755
index 000000000..7f0b80a13
--- /dev/null
+++ b/print/templates/email/email-verify/email-verify.js
@@ -0,0 +1,17 @@
+const Component = require(`vn-print/core/component`);
+const emailHeader = new Component('email-header');
+const emailFooter = new Component('email-footer');
+
+module.exports = {
+ name: 'email-verify',
+ components: {
+ 'email-header': emailHeader.build(),
+ 'email-footer': emailFooter.build()
+ },
+ props: {
+ url: {
+ type: [String],
+ required: true
+ }
+ }
+};
diff --git a/print/templates/email/email-verify/locale/en.yml b/print/templates/email/email-verify/locale/en.yml
new file mode 100644
index 000000000..0298f53b4
--- /dev/null
+++ b/print/templates/email/email-verify/locale/en.yml
@@ -0,0 +1,3 @@
+subject: Email Verify
+title: Email Verify
+click: Click on the following link to verify this email. If you haven't requested this email, just ignore it
diff --git a/print/templates/email/email-verify/locale/es.yml b/print/templates/email/email-verify/locale/es.yml
new file mode 100644
index 000000000..37bd6ef27
--- /dev/null
+++ b/print/templates/email/email-verify/locale/es.yml
@@ -0,0 +1,3 @@
+subject: Verificar correo
+title: Verificar correo
+click: Pulsa en el siguiente link para verificar este correo. Si no has pedido este correo, simplemente ignóralo
diff --git a/print/templates/email/recover-password/assets/css/import.js b/print/templates/email/recover-password/assets/css/import.js
new file mode 100644
index 000000000..7360587f7
--- /dev/null
+++ b/print/templates/email/recover-password/assets/css/import.js
@@ -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();
+
diff --git a/print/templates/email/recover-password/assets/css/style.css b/print/templates/email/recover-password/assets/css/style.css
new file mode 100644
index 000000000..5db85befa
--- /dev/null
+++ b/print/templates/email/recover-password/assets/css/style.css
@@ -0,0 +1,5 @@
+.external-link {
+ border: 2px dashed #8dba25;
+ border-radius: 3px;
+ text-align: center
+}
\ No newline at end of file
diff --git a/print/templates/email/recover-password/locale/es.yml b/print/templates/email/recover-password/locale/es.yml
new file mode 100644
index 000000000..c72b108ee
--- /dev/null
+++ b/print/templates/email/recover-password/locale/es.yml
@@ -0,0 +1,3 @@
+subject: Recuperar contraseña
+title: Recuperar contraseña
+Click on the following link to change your password.: Pulsa en el siguiente link para cambiar tu contraseña.
diff --git a/print/templates/email/recover-password/recover-password.html b/print/templates/email/recover-password/recover-password.html
new file mode 100644
index 000000000..a654b3d5f
--- /dev/null
+++ b/print/templates/email/recover-password/recover-password.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
{{ $t('subject') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
diff --git a/print/templates/email/recover-password/recover-password.js b/print/templates/email/recover-password/recover-password.js
new file mode 100755
index 000000000..b589411a9
--- /dev/null
+++ b/print/templates/email/recover-password/recover-password.js
@@ -0,0 +1,17 @@
+const Component = require(`vn-print/core/component`);
+const emailHeader = new Component('email-header');
+const emailFooter = new Component('email-footer');
+
+module.exports = {
+ name: 'recover-password',
+ components: {
+ 'email-header': emailHeader.build(),
+ 'email-footer': emailFooter.build()
+ },
+ props: {
+ url: {
+ type: [String],
+ required: true
+ }
+ }
+};
diff --git a/print/templates/email/weekly-hour-record/assets/css/import.js b/print/templates/email/weekly-hour-record/assets/css/import.js
new file mode 100644
index 000000000..1582b82c5
--- /dev/null
+++ b/print/templates/email/weekly-hour-record/assets/css/import.js
@@ -0,0 +1,12 @@
+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`])
+ .mergeStyles();
+
diff --git a/print/templates/email/weekly-hour-record/locale/en.yml b/print/templates/email/weekly-hour-record/locale/en.yml
new file mode 100644
index 000000000..817e5451e
--- /dev/null
+++ b/print/templates/email/weekly-hour-record/locale/en.yml
@@ -0,0 +1,6 @@
+subject: Weekly time log
+title: Record of hours week {0} year {1}
+dear: Dear worker
+description: Access the following link:
+ {0}
+ Click 'SATISFIED' if you agree with the hours worked. Otherwise, press 'NOT SATISFIED', detailing the cause of the disagreement.
diff --git a/print/templates/email/weekly-hour-record/locale/es.yml b/print/templates/email/weekly-hour-record/locale/es.yml
new file mode 100644
index 000000000..b70862f16
--- /dev/null
+++ b/print/templates/email/weekly-hour-record/locale/es.yml
@@ -0,0 +1,6 @@
+subject: Registro de horas semanal
+title: Registro de horas semana {0} año {1}
+dear: Estimado trabajador
+description: Acceda al siguiente enlace:
+ {0}
+ Pulse 'CONFORME' si esta de acuerdo con las horas trabajadas. En caso contrario pulse 'NO CONFORME', detallando la causa de la disconformidad.
diff --git a/print/templates/email/weekly-hour-record/weekly-hour-record.html b/print/templates/email/weekly-hour-record/weekly-hour-record.html
new file mode 100644
index 000000000..84abb4c61
--- /dev/null
+++ b/print/templates/email/weekly-hour-record/weekly-hour-record.html
@@ -0,0 +1,9 @@
+
+
+
+
{{ $t('title', [week, year]) }}
+
{{$t('dear')}},
+
+
+
+
diff --git a/print/templates/email/weekly-hour-record/weekly-hour-record.js b/print/templates/email/weekly-hour-record/weekly-hour-record.js
new file mode 100755
index 000000000..8fdaea0ce
--- /dev/null
+++ b/print/templates/email/weekly-hour-record/weekly-hour-record.js
@@ -0,0 +1,23 @@
+const Component = require(`vn-print/core/component`);
+const emailBody = new Component('email-body');
+
+module.exports = {
+ name: 'weekly-hour-record',
+ components: {
+ 'email-body': emailBody.build()
+ },
+ props: {
+ week: {
+ type: Number,
+ required: true
+ },
+ year: {
+ type: Number,
+ required: true
+ },
+ url: {
+ type: String,
+ required: true
+ }
+ }
+};
diff --git a/print/templates/reports/collection-label/assets/css/style.css b/print/templates/reports/collection-label/assets/css/style.css
index fe1975445..597921c92 100644
--- a/print/templates/reports/collection-label/assets/css/style.css
+++ b/print/templates/reports/collection-label/assets/css/style.css
@@ -1,6 +1,6 @@
html {
font-family: "Roboto";
- margin-top: -7px;
+ margin-top: -6px;
}
* {
box-sizing: border-box;
@@ -9,7 +9,7 @@ html {
}
#vertical {
writing-mode: vertical-rl;
- height: 226px;
+ height: 240px;
margin-left: -13px;
}
.outline {
@@ -18,6 +18,7 @@ html {
}
#nickname {
font-size: 22px;
+ max-width: 50px;
}
#agencyDescripton {
font-size: 32px;
diff --git a/print/templates/reports/collection-label/collection-label.html b/print/templates/reports/collection-label/collection-label.html
index eeb82ac4a..6716d1fe5 100644
--- a/print/templates/reports/collection-label/collection-label.html
+++ b/print/templates/reports/collection-label/collection-label.html
@@ -1,35 +1,36 @@
-
-
-
-
-
-
-
- {{labelData.levelV}} |
- {{labelData.ticketFk}} ⬸ {{labelData.clientFk}} |
- {{labelData.shipped}} |
-
-
- |
- {{labelData.workerCode}} |
-
-
- {{labelData.labelCount}} |
-
-
- {{labelData.size}} |
-
-
- {{labelData.agencyDescription}} |
- {{labelData.lineCount}} |
-
-
- {{labelData.nickName}} |
- {{labelData.agencyHour}} |
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {{labelData.collectionFk ? `${labelData.collectionFk} ~ ${labelData.wagon}-${labelData.level}` : '-'.repeat(23)}}
+ |
+
+ {{labelData.clientFk ? `${labelData.ticketFk} « ${labelData.clientFk}` : labelData.ticketFk}}
+ |
+ {{labelData.shipped ? labelData.shipped : '---'}} |
+
+
+ |
+ {{labelData.workerCode ? labelData.workerCode : '---'}} |
+
+
+ {{labelData.labelCount ? labelData.labelCount : 0}} |
+
+
+ {{labelData.code == 'plant' ? labelData.size + 'cm' : labelData.volume + 'm³'}} |
+
+
+ {{labelData.agencyDescription}} |
+ {{labelData.lineCount ? labelData.lineCount : 0}} |
+
+
+ {{labelData.nickName ? labelData.nickName : '---'}} |
+ {{labelData.shipped ? labelData.shippedHour : labelData.zoneHour}} |
+
+
+
+
+
\ No newline at end of file
diff --git a/print/templates/reports/collection-label/collection-label.js b/print/templates/reports/collection-label/collection-label.js
index 7bc25ad79..d2d5f6417 100644
--- a/print/templates/reports/collection-label/collection-label.js
+++ b/print/templates/reports/collection-label/collection-label.js
@@ -40,7 +40,7 @@ module.exports = {
format: 'code128',
displayValue: false,
width: 3.8,
- height: 110,
+ height: 115,
});
return xmlSerializer.serializeToString(svgNode);
},
diff --git a/print/templates/reports/collection-label/options.json b/print/templates/reports/collection-label/options.json
index 175b3c1db..ae88e6c0c 100644
--- a/print/templates/reports/collection-label/options.json
+++ b/print/templates/reports/collection-label/options.json
@@ -2,8 +2,8 @@
"width": "10.4cm",
"height": "4.8cm",
"margin": {
- "top": "0cm",
- "right": "0.5cm",
+ "top": "0.3cm",
+ "right": "0.6cm",
"bottom": "0cm",
"left": "0cm"
},
diff --git a/print/templates/reports/collection-label/sql/labelsData.sql b/print/templates/reports/collection-label/sql/labelsData.sql
index 6f5b47a54..b799b289b 100644
--- a/print/templates/reports/collection-label/sql/labelsData.sql
+++ b/print/templates/reports/collection-label/sql/labelsData.sql
@@ -1,21 +1,23 @@
-SELECT c.itemPackingTypeFk,
- CONCAT(tc.collectionFk, ' ', LEFT(cc.code, 4)) color,
- CONCAT(tc.collectionFk, ' ', SUBSTRING('ABCDEFGH',tc.wagon, 1), '-', tc.`level`) levelV,
- tc.ticketFk,
- LEFT(COALESCE(et.description, zo.name, am.name),12) agencyDescription,
+SELECT tc.collectionFk,
+ SUBSTRING('ABCDEFGH', tc.wagon, 1) wagon,
+ tc.`level`,
+ t.id ticketFk,
+ COALESCE(et.description, zo.name, am.name) agencyDescription,
am.name,
t.clientFk,
- CONCAT(CAST(SUM(sv.volume) AS DECIMAL(5, 2)), 'm³') m3 ,
- CAST(IF(ic.code = 'plant', CONCAT(MAX(i.`size`),' cm'), COUNT(*)) AS CHAR) size,
+ CAST(SUM(sv.volume) AS DECIMAL(5, 2)) volume,
+ MAX(i.`size`) `size`,
+ ic.code,
w.code workerCode,
- tt.labelCount,
- IF(HOUR(t.shipped), TIME_FORMAT(t.shipped, '%H:%i'), TIME_FORMAT(zo.`hour`, '%H:%i')) agencyHour,
+ TIME_FORMAT(t.shipped, '%H:%i') shippedHour,
+ TIME_FORMAT(zo.`hour`, '%H:%i') zoneHour,
DATE_FORMAT(t.shipped, '%d/%m/%y') shipped,
- COUNT(*) lineCount,
- t.nickName
+ t.nickName,
+ tt.labelCount,
+ COUNT(*) lineCount
FROM vn.ticket t
- JOIN vn.ticketCollection tc ON tc.ticketFk = t.id
- JOIN vn.collection c ON c.id = tc.collectionFk
+ LEFT JOIN vn.ticketCollection tc ON tc.ticketFk = t.id
+ LEFT JOIN vn.collection c ON c.id = tc.collectionFk
LEFT JOIN vn.collectionColors cc ON cc.shelve = tc.`level`
AND cc.wagon = tc.wagon
AND cc.trainFk = c.trainFk
@@ -24,12 +26,12 @@ SELECT c.itemPackingTypeFk,
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.itemCategory ic ON ic.id = it.categoryFk
- JOIN vn.worker w ON w.id = c.workerFk
+ LEFT JOIN vn.worker w ON w.id = c.workerFk
JOIN vn.agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN vn.ticketTrolley tt ON tt.ticket = t.id
LEFT JOIN vn.`zone` zo ON t.zoneFk = zo.id
LEFT JOIN vn.routesMonitor rm ON rm.routeFk = t.routeFk
LEFT JOIN vn.expeditionTruck et ON et.id = rm.expeditionTruckFk
- WHERE tc.ticketFk IN (?)
+ WHERE t.id IN (?)
GROUP BY t.id
ORDER BY cc.`code`;
\ No newline at end of file