diff --git a/back/methods/account/privileges.js b/back/methods/account/privileges.js new file mode 100644 index 0000000000..df421125ed --- /dev/null +++ b/back/methods/account/privileges.js @@ -0,0 +1,58 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('privileges', { + description: 'Change role and hasGrant if user has privileges', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'The user id', + http: {source: 'path'} + }, + { + arg: 'roleFk', + type: 'number', + description: 'The new role for user', + }, + { + arg: 'hasGrant', + type: 'boolean', + description: 'Whether to has grant' + } + ], + http: { + path: `/:id/privileges`, + verb: 'POST' + } + }); + + Self.privileges = async function(ctx, id, roleFk, hasGrant, options) { + const models = Self.app.models; + const userId = ctx.req.accessToken.userId; + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const user = await models.Account.findById(userId, null, myOptions); + + if (!user.hasGrant) + throw new UserError(`You don't have enough privileges`); + + const userToUpdate = await models.Account.findById(id); + if (hasGrant != null) + return await userToUpdate.updateAttribute('hasGrant', hasGrant, myOptions); + if (!roleFk) return; + + const role = await models.Role.findById(roleFk, null, myOptions); + const hasRole = await models.Account.hasRole(userId, role.name, myOptions); + + if (!hasRole) + throw new UserError(`You don't have enough privileges`); + + await userToUpdate.updateAttribute('roleFk', roleFk, myOptions); + }; +}; diff --git a/back/methods/account/specs/privileges.spec.js b/back/methods/account/specs/privileges.spec.js new file mode 100644 index 0000000000..137c086711 --- /dev/null +++ b/back/methods/account/specs/privileges.spec.js @@ -0,0 +1,99 @@ +const models = require('vn-loopback/server/server').models; + +describe('account privileges()', () => { + const employeeId = 1; + const developerId = 9; + const sysadminId = 66; + const bruceWayneId = 1101; + + it('should throw an error when user not has privileges', async() => { + const ctx = {req: {accessToken: {userId: developerId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + await models.Account.privileges(ctx, employeeId, null, true, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`You don't have enough privileges`); + }); + + it('should throw an error when user has privileges but not has the role', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + const root = await models.Role.findOne({ + where: { + name: 'root' + } + }, options); + await models.Account.privileges(ctx, employeeId, root.id, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`You don't have enough privileges`); + }); + + it('should change role', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + const options = {transaction: tx}; + const agency = await models.Role.findOne({ + where: { + name: 'agency' + } + }, options); + + let error; + let result; + try { + await models.Account.privileges(ctx, bruceWayneId, agency.id, null, options); + result = await models.Account.findById(bruceWayneId, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error).not.toBeDefined(); + expect(result.roleFk).toEqual(agency.id); + }); + + it('should change hasGrant', async() => { + const ctx = {req: {accessToken: {userId: sysadminId}}}; + const tx = await models.Account.beginTransaction({}); + + let error; + let result; + try { + const options = {transaction: tx}; + await models.Account.privileges(ctx, bruceWayneId, null, true, options); + result = await models.Account.findById(bruceWayneId, null, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error).not.toBeDefined(); + expect(result.hasGrant).toBeTruthy(); + }); +}); diff --git a/back/methods/dms/deleteTrashFiles.js b/back/methods/dms/deleteTrashFiles.js index e8b342ccc2..6f9a2a211d 100644 --- a/back/methods/dms/deleteTrashFiles.js +++ b/back/methods/dms/deleteTrashFiles.js @@ -17,14 +17,16 @@ module.exports = Self => { }); Self.deleteTrashFiles = async options => { - const tx = await Self.beginTransaction({}); + let tx; const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - if (!myOptions.transaction) + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); myOptions.transaction = tx; + } try { if (process.env.NODE_ENV == 'test') @@ -61,10 +63,9 @@ module.exports = Self => { const dstFolder = path.join(dmsContainer.client.root, pathHash); try { await fs.rmdir(dstFolder); - await dms.destroy(myOptions); - } catch (err) { - await dms.destroy(myOptions); - } + } catch (err) {} + + await dms.destroy(myOptions); } if (tx) await tx.commit(); } catch (e) { diff --git a/back/models/account.js b/back/models/account.js index ba703c68d7..f74052b5c9 100644 --- a/back/models/account.js +++ b/back/models/account.js @@ -7,6 +7,7 @@ module.exports = Self => { require('../methods/account/change-password')(Self); require('../methods/account/set-password')(Self); require('../methods/account/validate-token')(Self); + require('../methods/account/privileges')(Self); // Validations @@ -77,7 +78,7 @@ module.exports = Self => { `SELECT r.name FROM account.user u JOIN account.roleRole rr ON rr.role = u.role - JOIN account.role r ON r.id = rr.inheritsFrom + JOIN account.role r ON r.id = rr.inheritsFrom WHERE u.id = ?`, [userId], options); let roles = []; diff --git a/back/models/account.json b/back/models/account.json index 5f0b05f9bd..c25cd532d0 100644 --- a/back/models/account.json +++ b/back/models/account.json @@ -48,6 +48,9 @@ }, "image": { "type": "string" + }, + "hasGrant": { + "type": "boolean" } }, "relations": { diff --git a/db/changes/10490-august/00-user_hasGrant.sql b/db/changes/10490-august/00-user_hasGrant.sql new file mode 100644 index 0000000000..60d1273d83 --- /dev/null +++ b/db/changes/10490-august/00-user_hasGrant.sql @@ -0,0 +1 @@ +ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL; diff --git a/db/changes/10491-august/delete.keep b/db/changes/10491-august/delete.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 73a6ef6875..7e59c1a54a 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2663,3 +2663,7 @@ INSERT INTO `vn`.`collection` (`id`, `created`, `workerFk`, `stateFk`, `itemPack INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`) VALUES (9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL); + +UPDATE `account`.`user` + SET `hasGrant` = 1 + WHERE `id` = 66; diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index df9088140c..cd6d39795b 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -51,14 +51,12 @@ export default { accountDescriptor: { menuButton: 'vn-user-descriptor vn-icon-button[icon="more_vert"]', deleteAccount: '.vn-menu [name="deleteUser"]', - changeRole: '.vn-menu [name="changeRole"]', setPassword: '.vn-menu [name="setPassword"]', activateAccount: '.vn-menu [name="enableAccount"]', activateUser: '.vn-menu [name="activateUser"]', deactivateUser: '.vn-menu [name="deactivateUser"]', newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]', repeatPassword: 'vn-textfield[ng-model="$ctrl.repeatPassword"]', - newRole: 'vn-autocomplete[ng-model="$ctrl.newRole"]', activeAccountIcon: 'vn-icon[icon="contact_mail"]', activeUserIcon: 'vn-icon[icon="icon-disabled"]', acceptButton: 'button[response="accept"]', @@ -143,6 +141,11 @@ export default { verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]', save: 'vn-account-samba vn-submit' }, + accountPrivileges: { + checkHasGrant: 'vn-user-privileges vn-check[ng-model="$ctrl.user.hasGrant"]', + role: 'vn-user-privileges vn-autocomplete[ng-model="$ctrl.user.roleFk"]', + save: 'vn-user-privileges vn-submit' + }, clientsIndex: { createClientButton: `vn-float-button` }, @@ -1019,8 +1022,8 @@ export default { }, travelExtraCommunity: { anySearchResult: 'vn-travel-extra-community > vn-card div > tbody > tr[ng-attr-id="{{::travel.id}}"]', - firstTravelReference: 'vn-travel-extra-community tbody:nth-child(2) vn-textfield[ng-model="travel.ref"]', - firstTravelLockedKg: 'vn-travel-extra-community tbody:nth-child(2) vn-input-number[ng-model="travel.kg"]', + firstTravelReference: 'vn-travel-extra-community tbody:nth-child(2) vn-td-editable[name="reference"]', + firstTravelLockedKg: 'vn-travel-extra-community tbody:nth-child(2) vn-td-editable[name="lockedKg"]', removeContinentFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(3) > vn-icon > i' }, travelBasicData: { diff --git a/e2e/paths/10-travel/04_extra_community.spec.js b/e2e/paths/10-travel/04_extra_community.spec.js index a1cad6a7d1..c5975c9583 100644 --- a/e2e/paths/10-travel/04_extra_community.spec.js +++ b/e2e/paths/10-travel/04_extra_community.spec.js @@ -19,10 +19,10 @@ describe('Travel extra community path', () => { it('should edit the travel reference and the locked kilograms', async() => { await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter); await page.waitForSpinnerLoad(); - await page.clearInput(selectors.travelExtraCommunity.firstTravelReference); - await page.write(selectors.travelExtraCommunity.firstTravelReference, 'edited reference'); - await page.clearInput(selectors.travelExtraCommunity.firstTravelLockedKg); - await page.write(selectors.travelExtraCommunity.firstTravelLockedKg, '1500'); + await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference'); + await page.waitForSpinnerLoad(); + await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelLockedKg, '1500'); + const message = await page.waitForSnackbar(); expect(message.text).toContain('Data saved!'); @@ -32,9 +32,9 @@ describe('Travel extra community path', () => { await page.accessToSection('travel.index'); await page.accessToSection('travel.extraCommunity'); await page.waitToClick(selectors.travelExtraCommunity.removeContinentFilter); - - const reference = await page.waitToGetProperty(selectors.travelExtraCommunity.firstTravelReference, 'value'); - const lockedKg = await page.waitToGetProperty(selectors.travelExtraCommunity.firstTravelLockedKg, 'value'); + await page.waitForTextInElement(selectors.travelExtraCommunity.firstTravelReference, 'edited reference'); + const reference = await page.getProperty(selectors.travelExtraCommunity.firstTravelReference, 'innerText'); + const lockedKg = await page.getProperty(selectors.travelExtraCommunity.firstTravelLockedKg, 'innerText'); expect(reference).toContain('edited reference'); expect(lockedKg).toContain(1500); diff --git a/e2e/paths/14-account/01_create_and_basic_data.spec.js b/e2e/paths/14-account/01_create_and_basic_data.spec.js index 0400fb99e2..54e4d1f125 100644 --- a/e2e/paths/14-account/01_create_and_basic_data.spec.js +++ b/e2e/paths/14-account/01_create_and_basic_data.spec.js @@ -62,27 +62,6 @@ describe('Account create and basic data path', () => { }); describe('Descriptor option', () => { - describe('Edit role', () => { - it('should edit the role using the descriptor menu', async() => { - await page.waitToClick(selectors.accountDescriptor.menuButton); - await page.waitToClick(selectors.accountDescriptor.changeRole); - await page.autocompleteSearch(selectors.accountDescriptor.newRole, 'adminBoss'); - await page.waitToClick(selectors.accountDescriptor.acceptButton); - const message = await page.waitForSnackbar(); - - expect(message.text).toContain('Role changed succesfully!'); - }); - - it('should reload the roles section to see now there are more roles', async() => { - // when role updates db takes time to return changes, without this timeout the result would have been 3 - await page.waitForTimeout(1000); - await page.reloadSection('account.card.roles'); - const rolesCount = await page.countElement(selectors.accountRoles.anyResult); - - expect(rolesCount).toEqual(61); - }); - }); - describe('activate account', () => { it(`should check the active account icon isn't present in the descriptor`, async() => { await page.waitForNumberOfElements(selectors.accountDescriptor.activeAccountIcon, 0); diff --git a/e2e/paths/14-account/09_privileges.spec.js b/e2e/paths/14-account/09_privileges.spec.js new file mode 100644 index 0000000000..71e9345a8c --- /dev/null +++ b/e2e/paths/14-account/09_privileges.spec.js @@ -0,0 +1,86 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Account privileges path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('developer', 'account'); + await page.accessToSearchResult('1101'); + await page.accessToSection('account.card.privileges'); + }); + + afterAll(async() => { + await browser.close(); + }); + + describe('as developer', () => { + it('should throw error when give privileges', async() => { + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You don't have enough privileges`); + }); + + it('should throw error when change role', async() => { + await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee'); + await page.waitToClick(selectors.accountPrivileges.save); + + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You don't have enough privileges`); + }); + }); + + describe('as sysadmin', () => { + beforeAll(async() => { + await page.loginAndModule('sysadmin', 'account'); + await page.accessToSearchResult('9'); + await page.accessToSection('account.card.privileges'); + }); + + it('should give privileges', async() => { + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + const message = await page.waitForSnackbar(); + + await page.reloadSection('account.card.privileges'); + const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant); + + expect(message.text).toContain(`Data saved!`); + expect(result).toBe('checked'); + }); + + it('should change role', async() => { + await page.autocompleteSearch(selectors.accountPrivileges.role, 'employee'); + await page.waitToClick(selectors.accountPrivileges.save); + const message = await page.waitForSnackbar(); + + await page.reloadSection('account.card.privileges'); + const result = await page.waitToGetProperty(selectors.accountPrivileges.role, 'value'); + + expect(message.text).toContain(`Data saved!`); + expect(result).toContain('employee'); + }); + }); + + describe('as developer again', () => { + it('should remove privileges', async() => { + await page.accessToSearchResult('9'); + await page.accessToSection('account.card.privileges'); + + await page.waitToClick(selectors.accountPrivileges.checkHasGrant); + await page.waitToClick(selectors.accountPrivileges.save); + + await page.reloadSection('account.card.privileges'); + const result = await page.checkboxState(selectors.accountPrivileges.checkHasGrant); + + expect(result).toBe('unchecked'); + }); + }); +}); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 4162761daf..e5a0fae322 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -132,5 +132,6 @@ "Descanso diario 9h.": "Daily rest 9h.", "Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.", "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" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 349dbc5df4..67370b3438 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -96,7 +96,7 @@ "This postcode already exists": "Este código postal ya existe", "Concept cannot be blank": "El concepto no puede quedar en blanco", "File doesn't exists": "El archivo no existe", - "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias", + "You don't have privileges to change the zone": "No tienes permisos para cambiar la zona o para esos parámetros hay más de una opción de envío, hable con las agencias", "This ticket is already on weekly tickets": "Este ticket ya está en tickets programados", "Ticket id cannot be blank": "El id de ticket no puede quedar en blanco", "Weekday cannot be blank": "El día de la semana no puede quedar en blanco", diff --git a/modules/account/front/descriptor/index.html b/modules/account/front/descriptor/index.html index c709c1ec02..7a7ba43f3e 100644 --- a/modules/account/front/descriptor/index.html +++ b/modules/account/front/descriptor/index.html @@ -11,14 +11,6 @@ translate> Delete - - Change role - - @@ -128,22 +120,6 @@ question="Are you sure you want to continue?" message="User will be deactivated"> - - - - - - - - - - - \ No newline at end of file + diff --git a/modules/account/front/descriptor/index.js b/modules/account/front/descriptor/index.js index 3f27b1f760..b802b2349c 100644 --- a/modules/account/front/descriptor/index.js +++ b/modules/account/front/descriptor/index.js @@ -30,20 +30,6 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('User removed'))); } - onChangeRole() { - this.newRole = this.user.role.id; - this.$.changeRole.show(); - } - - onChangeRoleAccept() { - const params = {roleFk: this.newRole}; - return this.$http.patch(`Accounts/${this.id}`, params) - .then(() => { - this.emit('change'); - this.vnApp.showSuccess(this.$t('Role changed succesfully!')); - }); - } - onChangePassClick(askOldPass) { this.$http.get('UserPasswords/findOne') .then(res => { diff --git a/modules/account/front/descriptor/index.spec.js b/modules/account/front/descriptor/index.spec.js index 8ee67a3046..f5e7aa7d43 100644 --- a/modules/account/front/descriptor/index.spec.js +++ b/modules/account/front/descriptor/index.spec.js @@ -30,17 +30,6 @@ describe('component vnUserDescriptor', () => { }); }); - describe('onChangeRoleAccept()', () => { - it('should call backend method to change role', () => { - $httpBackend.expectPATCH('Accounts/1').respond(); - controller.onChangeRoleAccept(); - $httpBackend.flush(); - - expect(controller.vnApp.showSuccess).toHaveBeenCalled(); - expect(controller.emit).toHaveBeenCalledWith('change'); - }); - }); - describe('onPassChange()', () => { it('should throw an error when password is empty', () => { expect(() => { diff --git a/modules/account/front/index.js b/modules/account/front/index.js index 50e04c9fa4..0cd0c49555 100644 --- a/modules/account/front/index.js +++ b/modules/account/front/index.js @@ -18,3 +18,4 @@ import './roles'; import './ldap'; import './samba'; import './accounts'; +import './privileges'; diff --git a/modules/account/front/privileges/index.html b/modules/account/front/privileges/index.html new file mode 100644 index 0000000000..e3e44898aa --- /dev/null +++ b/modules/account/front/privileges/index.html @@ -0,0 +1,42 @@ + + + +
+ + + + + + + + + + + + + + + + +
diff --git a/modules/account/front/privileges/index.js b/modules/account/front/privileges/index.js new file mode 100644 index 0000000000..00ba772df0 --- /dev/null +++ b/modules/account/front/privileges/index.js @@ -0,0 +1,9 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnUserPrivileges', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/account/front/privileges/locale/es.yml b/modules/account/front/privileges/locale/es.yml new file mode 100644 index 0000000000..f7330e1be0 --- /dev/null +++ b/modules/account/front/privileges/locale/es.yml @@ -0,0 +1,2 @@ +Privileges: Privilegios +Has grant: Tiene privilegios diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json index 66b26f4276..b96c931c95 100644 --- a/modules/account/front/routes.json +++ b/modules/account/front/routes.json @@ -19,7 +19,8 @@ {"state": "account.card.basicData", "icon": "settings"}, {"state": "account.card.roles", "icon": "group"}, {"state": "account.card.mailForwarding", "icon": "forward"}, - {"state": "account.card.aliases", "icon": "email"} + {"state": "account.card.aliases", "icon": "email"}, + {"state": "account.card.privileges", "icon": "badge"} ], "role": [ {"state": "account.role.card.basicData", "icon": "settings"}, @@ -99,6 +100,13 @@ "description": "Mail aliases", "acl": ["marketing", "hr"] }, + { + "url": "/privileges", + "state": "account.card.privileges", + "component": "vn-user-privileges", + "description": "Privileges", + "acl": ["hr"] + }, { "url": "/role?q", "state": "account.role", @@ -249,4 +257,4 @@ "acl": ["developer"] } ] -} \ No newline at end of file +} diff --git a/modules/claim/back/methods/claim/claimPickupEmail.js b/modules/claim/back/methods/claim/claimPickupEmail.js index b946353d65..4d64cc66ea 100644 --- a/modules/claim/back/methods/claim/claimPickupEmail.js +++ b/modules/claim/back/methods/claim/claimPickupEmail.js @@ -1,8 +1,9 @@ -const {Report, Email, smtp} = require('vn-print'); +const {Email} = require('vn-print'); module.exports = Self => { Self.remoteMethodCtx('claimPickupEmail', { description: 'Sends the the claim pickup order email with an attached PDF', + accessType: 'WRITE', accepts: [ { arg: 'id', @@ -40,7 +41,7 @@ module.exports = Self => { } }); - Self.claimPickupEmail = async(ctx, id) => { + Self.claimPickupEmail = async ctx => { const args = Object.assign({}, ctx.args); const params = { recipient: args.recipient, diff --git a/modules/client/back/methods/client/consumptionSendQueued.js b/modules/client/back/methods/client/consumptionSendQueued.js index 3f551d3d29..77e0e34f2a 100644 --- a/modules/client/back/methods/client/consumptionSendQueued.js +++ b/modules/client/back/methods/client/consumptionSendQueued.js @@ -18,46 +18,46 @@ module.exports = Self => { Self.consumptionSendQueued = async() => { const queues = await Self.rawSql(` SELECT - ccq.id, - c.id AS clientFk, - c.email AS clientEmail, - eu.email salesPersonEmail, - REPLACE(json_extract(params, '$.from'), '"', '') AS fromDate, - REPLACE(json_extract(params, '$.to'), '"', '') AS toDate - FROM clientConsumptionQueue ccq - JOIN client c ON ( - JSON_SEARCH( - JSON_ARRAY( - json_extract(params, '$.clients') - ) - , 'all', c.id) IS NOT NULL) - JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - JOIN ticket t ON t.clientFk = c.id - JOIN sale s ON s.ticketFk = t.id - JOIN item i ON i.id = s.itemFk - JOIN itemType it ON it.id = i.typeFk - WHERE status = '' - AND it.isPackaging = FALSE - AND DATE(t.shipped) BETWEEN - REPLACE(json_extract(params, '$.from'), '"', '') AND - REPLACE(json_extract(params, '$.to'), '"', '') - GROUP BY c.id`); + id, + params + FROM clientConsumptionQueue + WHERE status = ''`); for (const queue of queues) { try { - const args = { - id: queue.clientFk, - recipient: queue.clientEmail, - replyTo: queue.salesPersonEmail, - from: queue.fromDate, - to: queue.toDate - }; + const params = JSON.parse(queue.params); - const email = new Email('campaign-metrics', args); - await email.send(); + const clients = await Self.rawSql(` + SELECT + c.id AS clientFk, + c.email AS clientEmail, + eu.email salesPersonEmail + FROM client c + JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk + JOIN ticket t ON t.clientFk = c.id + JOIN sale s ON s.ticketFk = t.id + JOIN item i ON i.id = s.itemFk + JOIN itemType it ON it.id = i.typeFk + WHERE c.id IN(?) + AND it.isPackaging = FALSE + AND DATE(t.shipped) BETWEEN ? AND ? + GROUP BY c.id`, [params.clients, params.from, params.to]); + + for (const client of clients) { + const args = { + id: client.clientFk, + recipient: client.clientEmail, + replyTo: client.salesPersonEmail, + from: params.from, + to: params.to + }; + + const email = new Email('campaign-metrics', args); + await email.send(); + } await Self.rawSql(` - UPDATE clientConsumptionQueue + UPDATE clientConsumptionQueue SET status = 'printed', printed = ? WHERE id = ?`, @@ -69,7 +69,7 @@ module.exports = Self => { WHERE id = ?`, [error.message, queue.id]); - throw e; + throw error; } } diff --git a/modules/client/front/notification/index.js b/modules/client/front/notification/index.js index e70af12b2f..faa062b253 100644 --- a/modules/client/front/notification/index.js +++ b/modules/client/front/notification/index.js @@ -77,15 +77,16 @@ export default class Controller extends Section { onSendClientConsumption() { const clientIds = this.checked.map(client => client.id); - const params = { + const data = { clients: clientIds, from: this.campaign.from, to: this.campaign.to }; + const params = JSON.stringify(data); this.$http.post('ClientConsumptionQueues', {params}) .then(() => this.$.filters.hide()) - .then(() => this.vnApp.showSuccess(this.$t('Notifications sent!'))); + .then(() => this.vnApp.showSuccess(this.$t('Notification sent!'))); } exprBuilder(param, value) { diff --git a/modules/client/front/notification/index.spec.js b/modules/client/front/notification/index.spec.js index ea082c4038..4e754f6ad8 100644 --- a/modules/client/front/notification/index.spec.js +++ b/modules/client/front/notification/index.spec.js @@ -69,15 +69,16 @@ describe('Client notification', () => { data[0].$checked = true; data[1].$checked = true; - const params = Object.assign({ + const args = Object.assign({ clients: [1101, 1102] }, controller.campaign); + const params = JSON.stringify(args); $httpBackend.expect('POST', `ClientConsumptionQueues`, {params}).respond(200, params); controller.onSendClientConsumption(); $httpBackend.flush(); - expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Notifications sent!'); + expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Notification sent!'); }); }); diff --git a/modules/item/front/diary/index.js b/modules/item/front/diary/index.js index 6d912ebe8a..c997ea4918 100644 --- a/modules/item/front/diary/index.js +++ b/modules/item/front/diary/index.js @@ -27,7 +27,7 @@ class Controller extends Section { if (this.$params.warehouseFk) this.warehouseFk = this.$params.warehouseFk; else if (value) - this.warehouseFk = value.itemType.warehouseFk; + this.warehouseFk = this.vnConfig.warehouseFk; if (this.$params.lineFk) this.lineFk = this.$params.lineFk; diff --git a/modules/item/front/diary/index.spec.js b/modules/item/front/diary/index.spec.js index 78dae08832..a80fbfc690 100644 --- a/modules/item/front/diary/index.spec.js +++ b/modules/item/front/diary/index.spec.js @@ -19,7 +19,8 @@ describe('Item', () => { describe('set item()', () => { it('should set warehouseFk property based on itemType warehouseFk', () => { jest.spyOn(controller.$, '$applyAsync'); - controller.item = {id: 1, itemType: {warehouseFk: 1}}; + controller.vnConfig = {warehouseFk: 1}; + controller.item = {id: 1}; expect(controller.$.$applyAsync).toHaveBeenCalledWith(jasmine.any(Function)); $scope.$apply(); diff --git a/modules/monitor/back/methods/sales-monitor/salesFilter.js b/modules/monitor/back/methods/sales-monitor/salesFilter.js index ff642b0883..7be130dda8 100644 --- a/modules/monitor/back/methods/sales-monitor/salesFilter.js +++ b/modules/monitor/back/methods/sales-monitor/salesFilter.js @@ -211,7 +211,7 @@ module.exports = Self => { LEFT JOIN province p ON p.id = a.provinceFk LEFT JOIN warehouse w ON w.id = t.warehouseFk LEFT JOIN agencyMode am ON am.id = t.agencyModeFk - STRAIGHT_JOIN ticketState ts ON ts.ticketFk = t.id + LEFT JOIN ticketState ts ON ts.ticketFk = t.id LEFT JOIN state st ON st.id = ts.stateFk LEFT JOIN client c ON c.id = t.clientFk LEFT JOIN worker wk ON wk.id = c.salesPersonFk diff --git a/modules/route/back/methods/route/driverRouteEmail.js b/modules/route/back/methods/route/driverRouteEmail.js index 81d7703607..4d5279d2d4 100644 --- a/modules/route/back/methods/route/driverRouteEmail.js +++ b/modules/route/back/methods/route/driverRouteEmail.js @@ -3,6 +3,7 @@ const {Email} = require('vn-print'); module.exports = Self => { Self.remoteMethodCtx('driverRouteEmail', { description: 'Sends the driver route email with an attached PDF', + accessType: 'WRITE', accepts: [ { arg: 'id', diff --git a/modules/ticket/back/methods/ticket-request/getItemTypeWorker.js b/modules/ticket/back/methods/ticket-request/getItemTypeWorker.js new file mode 100644 index 0000000000..0655c7bba4 --- /dev/null +++ b/modules/ticket/back/methods/ticket-request/getItemTypeWorker.js @@ -0,0 +1,57 @@ +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; + +module.exports = Self => { + Self.remoteMethodCtx('getItemTypeWorker', { + description: 'Returns the workers that appear in itemType', + accessType: 'READ', + accepts: [{ + arg: 'filter', + type: 'Object', + description: 'Filter defining where and paginated data', + required: true + }], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/getItemTypeWorker`, + verb: 'GET' + } + }); + + Self.getItemTypeWorker = async(ctx, filter, options) => { + const myOptions = {}; + const conn = Self.dataSource.connector; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const query = + `SELECT DISTINCT u.id, u.nickname + FROM itemType it + JOIN worker w ON w.id = it.workerFk + JOIN account.user u ON u.id = w.id`; + + let stmt = new ParameterizedSQL(query); + + if (filter.where) { + const value = filter.where.firstName; + const myFilter = { + where: {or: [ + {'w.firstName': {like: `%${value}%`}}, + {'w.lastName': {like: `%${value}%`}}, + {'u.name': {like: `%${value}%`}}, + {'u.nickname': {like: `%${value}%`}} + ]} + }; + + stmt.merge(conn.makeSuffix(myFilter)); + } + + if (tx) await tx.commit(); + + return conn.executeStmt(stmt); + }; +}; diff --git a/modules/ticket/back/methods/ticket-request/specs/getItemTypeWorkers.spec.js b/modules/ticket/back/methods/ticket-request/specs/getItemTypeWorkers.spec.js new file mode 100644 index 0000000000..ae5c508b6d --- /dev/null +++ b/modules/ticket/back/methods/ticket-request/specs/getItemTypeWorkers.spec.js @@ -0,0 +1,21 @@ +const models = require('vn-loopback/server/server').models; + +describe('ticket-request getItemTypeWorker()', () => { + const ctx = {req: {accessToken: {userId: 18}}}; + + it('should return the buyer as result', async() => { + const filter = {where: {firstName: 'buyer'}}; + + const result = await models.TicketRequest.getItemTypeWorker(ctx, filter); + + expect(result.length).toEqual(1); + }); + + it('should return the workers at itemType as result', async() => { + const filter = {}; + + const result = await models.TicketRequest.getItemTypeWorker(ctx, filter); + + expect(result.length).toBeGreaterThan(1); + }); +}); diff --git a/modules/ticket/back/methods/ticket/closeAll.js b/modules/ticket/back/methods/ticket/closeAll.js index 4f3813eb83..5e81e38274 100644 --- a/modules/ticket/back/methods/ticket/closeAll.js +++ b/modules/ticket/back/methods/ticket/closeAll.js @@ -18,6 +18,7 @@ module.exports = Self => { Self.closeAll = async() => { const toDate = new Date(); + toDate.setHours(0, 0, 0, 0); toDate.setDate(toDate.getDate() - 1); const todayMinDate = new Date(); diff --git a/modules/ticket/back/methods/ticket/closure.js b/modules/ticket/back/methods/ticket/closure.js index 3cbc85f96f..d5fa58e7bd 100644 --- a/modules/ticket/back/methods/ticket/closure.js +++ b/modules/ticket/back/methods/ticket/closure.js @@ -101,6 +101,7 @@ module.exports = async function(Self, tickets, reqArgs = {}) { if (firstOrder == 1) { const args = { id: ticket.clientFk, + companyId: ticket.companyFk, recipientId: ticket.clientFk, recipient: ticket.recipient, replyTo: ticket.salesPersonEmail @@ -109,7 +110,7 @@ module.exports = async function(Self, tickets, reqArgs = {}) { const email = new Email('incoterms-authorization', args); await email.send(); - const sample = await Self.rawSql( + const [sample] = await Self.rawSql( `SELECT id FROM sample WHERE code = 'incoterms-authorization' diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index 6b385992c4..baa6a0b41b 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -126,8 +126,7 @@ module.exports = Self => { myOptions); if (!zoneShipped || zoneShipped.zoneFk != args.zoneFk) { - const error = `You don't have privileges to change the zone or - for these parameters there are more than one shipping options, talk to agencies`; + const error = `You don't have privileges to change the zone`; throw new UserError(error); } diff --git a/modules/ticket/back/methods/ticket/priceDifference.js b/modules/ticket/back/methods/ticket/priceDifference.js index 0e8cc2e068..989e0e5cec 100644 --- a/modules/ticket/back/methods/ticket/priceDifference.js +++ b/modules/ticket/back/methods/ticket/priceDifference.js @@ -88,8 +88,7 @@ module.exports = Self => { myOptions); if (!zoneShipped || zoneShipped.zoneFk != args.zoneId) { - const error = `You don't have privileges to change the zone or - for these parameters there are more than one shipping options, talk to agencies`; + const error = `You don't have privileges to change the zone`; throw new UserError(error); } diff --git a/modules/ticket/back/models/ticket-request.js b/modules/ticket/back/models/ticket-request.js index 234978f333..4125126dcd 100644 --- a/modules/ticket/back/models/ticket-request.js +++ b/modules/ticket/back/models/ticket-request.js @@ -5,6 +5,7 @@ module.exports = function(Self) { require('../methods/ticket-request/filter')(Self); require('../methods/ticket-request/deny')(Self); require('../methods/ticket-request/confirm')(Self); + require('../methods/ticket-request/getItemTypeWorker')(Self); Self.observe('before save', async function(ctx) { if (ctx.isNewInstance) { diff --git a/modules/ticket/front/request/create/index.html b/modules/ticket/front/request/create/index.html index 8d25358b52..71a2de2608 100644 --- a/modules/ticket/front/request/create/index.html +++ b/modules/ticket/front/request/create/index.html @@ -18,9 +18,8 @@ diff --git a/modules/travel/front/extra-community/index.html b/modules/travel/front/extra-community/index.html index f19ab592e7..5174f8da25 100644 --- a/modules/travel/front/extra-community/index.html +++ b/modules/travel/front/extra-community/index.html @@ -42,10 +42,10 @@ Id - + Supplier - + Agency @@ -100,37 +100,37 @@ {{::travel.id}} - + {{::travel.cargoSupplierNickname}} - {{::travel.agencyModeName}} - - - - + {{::travel.agencyModeName}} + + + {{travel.ref}} + + + + + {{::travel.stickers}} - - - + + + {{travel.kg}} + + + + + {{::travel.loadedKg}} {{::travel.volumeKg}} @@ -150,7 +150,7 @@ {{::entry.id}} - + @@ -158,7 +158,7 @@ - {{::entry.ref}} + {{::entry.ref}} {{::entry.stickers}} {{::entry.loadedkg}} diff --git a/modules/travel/front/extra-community/style.scss b/modules/travel/front/extra-community/style.scss index 532a3056ab..fb64822f97 100644 --- a/modules/travel/front/extra-community/style.scss +++ b/modules/travel/front/extra-community/style.scss @@ -3,7 +3,6 @@ vn-travel-extra-community { .header { margin-bottom: 16px; - font-size: 1.25rem; line-height: 1; padding: 7px; padding-bottom: 7px; @@ -16,6 +15,10 @@ vn-travel-extra-community { overflow: hidden; text-overflow: ellipsis; cursor: pointer; + .multi-line{ + padding-top: 15px; + padding-bottom: 15px; + } } table[vn-droppable] { @@ -29,10 +32,10 @@ vn-travel-extra-community { outline: 0; height: 65px; pointer-events: fill; - user-select:all; + user-select: all; } - tr[draggable] *::selection{ + tr[draggable] *::selection { background-color: transparent; } @@ -43,16 +46,22 @@ vn-travel-extra-community { tr[draggable].dragging { background-color: $color-primary-light; color: $color-font-light; - font-weight:bold; + font-weight: bold; } - .td-editable{ - input{ - font-size: 1.25rem!important; - } + + .multi-line{ + max-width: 200px; + word-wrap: normal; + white-space: normal; } - .number *{ - text-align: right; + vn-td-editable text { + background-color: transparent; + padding: 0; + border: 0; + border-bottom: 1px dashed $color-active; + border-radius: 0; + color: $color-active } } diff --git a/modules/zone/back/methods/zone/getLeaves.js b/modules/zone/back/methods/zone/getLeaves.js index db17beb1bb..ed421e3397 100644 --- a/modules/zone/back/methods/zone/getLeaves.js +++ b/modules/zone/back/methods/zone/getLeaves.js @@ -59,7 +59,10 @@ module.exports = Self => { } const leaves = map.get(parentId); - setLeaves(leaves); + + const maxNodes = 250; + if (res.length <= maxNodes) + setLeaves(leaves); return leaves || []; };