From b3d333368bdd40777e43159cdbe8b1511ff1e4bf Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 23 Mar 2021 16:19:45 +0100 Subject: [PATCH 1/5] #2819 - Notify worker assignment --- loopback/locale/es.json | 4 +- modules/client/back/models/client.js | 109 +++++++++++++----- .../client/back/models/specs/client.spec.js | 58 ++++++++++ 3 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 modules/client/back/models/specs/client.spec.js diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 96858eb4a..b325b9176 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -176,5 +176,7 @@ "Invalid account": "Cuenta inválida", "Compensation account is empty": "La cuenta para compensar está vacia", "This genus already exist": "Este genus ya existe", - "This specie already exist": "Esta especie ya existe" + "This specie already exist": "Esta especie ya existe", + "Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})", + "None": "Ninguno" } \ No newline at end of file diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 1e535a5d7..c30270f6f 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -226,36 +226,10 @@ module.exports = Self => { }; await Self.app.models.ClientCredit.create(newCredit); } - }); - const app = require('vn-loopback/server/server'); - app.on('started', function() { - let account = app.models.Account; - account.observe('before save', async ctx => { - if (ctx.isNewInstance) return; - ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance)); - }); - - account.observe('after save', async ctx => { - let changes = ctx.data || ctx.instance; - if (!ctx.isNewInstance && changes) { - let oldData = ctx.hookState.oldInstance; - let hasChanges = oldData.name != changes.name || oldData.active != changes.active; - if (!hasChanges) return; - - let userId = ctx.options.accessToken.userId; - let logRecord = { - originFk: oldData.id, - userFk: userId, - action: 'update', - changedModel: 'Account', - oldInstance: {name: oldData.name, active: oldData.active}, - newInstance: {name: changes.name, active: changes.active} - }; - await Self.app.models.ClientLog.create(logRecord); - } - }); + ctx.hookState.oldInstance = Object.assign({}, orgData.__data); + ctx.hookState.newInstance = Object.assign({}, finalState); }); Self.observe('after save', async ctx => { @@ -303,8 +277,57 @@ module.exports = Self => { query: params }); } + + const workerIdBefore = oldInstance.salesPersonFk; + const workerIdAfter = newInstance.salesPersonFk; + const assignmentChanged = workerIdBefore != workerIdAfter; + if (assignmentChanged) + await notifyAssignment(instance, workerIdBefore, workerIdAfter); }); + // Send notification on client worker assignment + async function notifyAssignment(client, previousWorkerId, currentWorkerId) { + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const httpRequest = httpCtx.req.http.req; + const $t = httpRequest.__; + const headers = httpRequest.headers; + const origin = headers.origin; + const models = Self.app.models; + + let previousWorkerName = $t('None'); + let currentWorkerName = $t('None'); + if (previousWorkerId) { + const worker = await models.Worker.findById(previousWorkerId, { + include: {relation: 'user'} + }); + previousWorkerName = worker && worker.user().nickname; + } + + if (currentWorkerId) { + const worker = await models.Worker.findById(currentWorkerId, { + include: {relation: 'user'} + }); + currentWorkerName = worker && worker.user().nickname; + } + + const fullUrl = `${origin}/#!/client/${client.id}/basic-data`; + const message = $t('Client assignment has changed', { + clientId: client.id, + clientName: client.name, + url: fullUrl, + previousWorkerName: previousWorkerName, + currentWorkerName: currentWorkerName + }); + + + if (previousWorkerName) // change to worker userName + await models.Chat.send(httpCtx, '@joan', message); + + if (currentWorkerName) + await models.Chat.send(httpCtx, '@joan', message); + } + async function validateCreditChange(ctx, finalState) { let models = Self.app.models; let userId = ctx.options.accessToken.userId; @@ -341,4 +364,34 @@ module.exports = Self => { if (count <= 0) throw new UserError('The role cannot set this credit amount'); } + + const app = require('vn-loopback/server/server'); + app.on('started', function() { + let account = app.models.Account; + + account.observe('before save', async ctx => { + if (ctx.isNewInstance) return; + ctx.hookState.oldInstance = JSON.parse(JSON.stringify(ctx.currentInstance)); + }); + + account.observe('after save', async ctx => { + let changes = ctx.data || ctx.instance; + if (!ctx.isNewInstance && changes) { + let oldData = ctx.hookState.oldInstance; + let hasChanges = oldData.name != changes.name || oldData.active != changes.active; + if (!hasChanges) return; + + let userId = ctx.options.accessToken.userId; + let logRecord = { + originFk: oldData.id, + userFk: userId, + action: 'update', + changedModel: 'Account', + oldInstance: {name: oldData.name, active: oldData.active}, + newInstance: {name: changes.name, active: changes.active} + }; + await Self.app.models.ClientLog.create(logRecord); + } + }); + }); }; diff --git a/modules/client/back/models/specs/client.spec.js b/modules/client/back/models/specs/client.spec.js new file mode 100644 index 000000000..685acc80d --- /dev/null +++ b/modules/client/back/models/specs/client.spec.js @@ -0,0 +1,58 @@ +const app = require('vn-loopback/server/server'); + +describe('loopback model address', () => { + let createdAddressId; + const clientId = 101; + + afterAll(async done => { + let client = await app.models.Client.findById(clientId); + + await app.models.Address.destroyById(createdAddressId); + await client.updateAttribute('isEqualizated', false); + + done(); + }); + + describe('observe()', () => { + it('should throw an error when deactivating a consignee if its the default address', async() => { + let error; + let address = await app.models.Address.findById(1); + + await address.updateAttribute('isActive', false) + .catch(e => { + error = e; + + expect(error.message).toEqual('The default consignee can not be unchecked'); + }); + + expect(error).toBeDefined(); + }); + + it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => { + let client = await app.models.Client.findById(clientId); + + expect(client.isEqualizated).toBeFalsy(); + + await client.updateAttribute('isEqualizated', true); + + let newAddress = await app.models.Address.create({ + clientFk: clientId, + agencyModeFk: 5, + city: 'here', + isActive: true, + mobile: '555555555', + nickname: 'Test address', + phone: '555555555', + postalCode: '46000', + provinceFk: 1, + street: 'Test address', + incotermsFk: 'FAS', + customsAgentFk: 1 + }); + + expect(newAddress.isEqualizated).toBeTruthy(); + + createdAddressId = newAddress.id; + }); + }); +}); From 32805dbafb5ea82ce8f31e126a41da9039ca9bfc Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 24 Mar 2021 12:45:26 +0100 Subject: [PATCH 2/5] Added model unit test --- modules/client/back/models/client.js | 36 +++++---- .../client/back/models/specs/client.spec.js | 78 +++++++++---------- 2 files changed, 56 insertions(+), 58 deletions(-) diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index c30270f6f..497fad00a 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -227,9 +227,10 @@ module.exports = Self => { await Self.app.models.ClientCredit.create(newCredit); } - - ctx.hookState.oldInstance = Object.assign({}, orgData.__data); - ctx.hookState.newInstance = Object.assign({}, finalState); + if (orgData) + ctx.hookState.oldInstance = Object.assign({}, orgData.__data); + if (finalState) + ctx.hookState.newInstance = Object.assign({}, finalState); }); Self.observe('after save', async ctx => { @@ -282,11 +283,11 @@ module.exports = Self => { const workerIdAfter = newInstance.salesPersonFk; const assignmentChanged = workerIdBefore != workerIdAfter; if (assignmentChanged) - await notifyAssignment(instance, workerIdBefore, workerIdAfter); + await Self.notifyAssignment(instance, workerIdBefore, workerIdAfter); }); // Send notification on client worker assignment - async function notifyAssignment(client, previousWorkerId, currentWorkerId) { + Self.notifyAssignment = async function notifyAssignment(client, previousWorkerId, currentWorkerId) { const loopBackContext = LoopBackContext.getCurrentContext(); const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; @@ -295,20 +296,22 @@ module.exports = Self => { const origin = headers.origin; const models = Self.app.models; - let previousWorkerName = $t('None'); - let currentWorkerName = $t('None'); + let previousWorker = {name: $t('None')}; + let currentWorker = {name: $t('None')}; if (previousWorkerId) { const worker = await models.Worker.findById(previousWorkerId, { include: {relation: 'user'} }); - previousWorkerName = worker && worker.user().nickname; + previousWorker.user = worker && worker.user().name; + previousWorker.name = worker && worker.user().nickname; } if (currentWorkerId) { const worker = await models.Worker.findById(currentWorkerId, { include: {relation: 'user'} }); - currentWorkerName = worker && worker.user().nickname; + currentWorker.user = worker && worker.user().name; + currentWorker.name = worker && worker.user().nickname; } const fullUrl = `${origin}/#!/client/${client.id}/basic-data`; @@ -316,17 +319,16 @@ module.exports = Self => { clientId: client.id, clientName: client.name, url: fullUrl, - previousWorkerName: previousWorkerName, - currentWorkerName: currentWorkerName + previousWorkerName: previousWorker.name, + currentWorkerName: currentWorker.name }); - - if (previousWorkerName) // change to worker userName - await models.Chat.send(httpCtx, '@joan', message); + if (previousWorkerId) + await models.Chat.send(httpCtx, `@${previousWorker.user}`, message); - if (currentWorkerName) - await models.Chat.send(httpCtx, '@joan', message); - } + if (currentWorkerId) + await models.Chat.send(httpCtx, `@${currentWorker.user}`, message); + }; async function validateCreditChange(ctx, finalState) { let models = Self.app.models; diff --git a/modules/client/back/models/specs/client.spec.js b/modules/client/back/models/specs/client.spec.js index 685acc80d..a9d479516 100644 --- a/modules/client/back/models/specs/client.spec.js +++ b/modules/client/back/models/specs/client.spec.js @@ -1,58 +1,54 @@ const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); -describe('loopback model address', () => { - let createdAddressId; - const clientId = 101; +describe('Client Model', () => { + const activeCtx = { + accessToken: {userId: 9}, + http: { + req: { + headers: {origin: 'http://localhost'}, + [`__`]: value => { + return value; + } + } + } + }; + const ctx = {req: activeCtx}; + const chatModel = app.models.Chat; + const client = {id: 101, name: 'Bruce Banner'}; + const previousWorkerId = 106; // DavidCharlesHaller + const currentWorkerId = 107; // HankPym - afterAll(async done => { - let client = await app.models.Client.findById(clientId); - - await app.models.Address.destroyById(createdAddressId); - await client.updateAttribute('isEqualizated', false); - - done(); + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); }); - describe('observe()', () => { - it('should throw an error when deactivating a consignee if its the default address', async() => { - let error; - let address = await app.models.Address.findById(1); + describe('notifyAssignment()', () => { + it('should call to the Chat send() method for both workers', async() => { + spyOn(chatModel, 'send').and.callThrough(); - await address.updateAttribute('isActive', false) - .catch(e => { - error = e; + await app.models.Client.notifyAssignment(client, previousWorkerId, currentWorkerId); - expect(error.message).toEqual('The default consignee can not be unchecked'); - }); - - expect(error).toBeDefined(); + expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`); + expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`); }); - it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => { - let client = await app.models.Client.findById(clientId); + it('should call to the Chat send() method for the previous worker', async() => { + spyOn(chatModel, 'send').and.callThrough(); - expect(client.isEqualizated).toBeFalsy(); + await app.models.Client.notifyAssignment(client, null, currentWorkerId); - await client.updateAttribute('isEqualizated', true); + expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`); + }); - let newAddress = await app.models.Address.create({ - clientFk: clientId, - agencyModeFk: 5, - city: 'here', - isActive: true, - mobile: '555555555', - nickname: 'Test address', - phone: '555555555', - postalCode: '46000', - provinceFk: 1, - street: 'Test address', - incotermsFk: 'FAS', - customsAgentFk: 1 - }); + it('should call to the Chat send() method for the current worker', async() => { + spyOn(chatModel, 'send').and.callThrough(); - expect(newAddress.isEqualizated).toBeTruthy(); + await app.models.Client.notifyAssignment(client, previousWorkerId, null); - createdAddressId = newAddress.id; + expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`); }); }); }); From e767e95729b5f3675c967ff9edff44b62c66eb28 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 24 Mar 2021 13:09:35 +0100 Subject: [PATCH 3/5] Loggable fixes --- loopback/common/models/loggable.js | 9 ++++++--- loopback/locale/en.json | 4 +++- modules/client/back/models/client.js | 5 ----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/loopback/common/models/loggable.js b/loopback/common/models/loggable.js index 557aad66a..956762be2 100644 --- a/loopback/common/models/loggable.js +++ b/loopback/common/models/loggable.js @@ -25,8 +25,8 @@ module.exports = function(Self) { if (ctx.data) { const changes = pick(ctx.currentInstance, Object.keys(ctx.data)); - newInstance = await fkToValue(ctx.data, ctx); - oldInstance = await fkToValue(changes, ctx); + newInstance = ctx.data; + oldInstance = changes; if (ctx.where && !ctx.currentInstance) { const fields = Object.keys(ctx.data); @@ -41,7 +41,7 @@ module.exports = function(Self) { // Get changes from created instance if (ctx.isNewInstance) - newInstance = await fkToValue(ctx.instance.__data, ctx); + newInstance = ctx.instance.__data; ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = newInstance; @@ -261,6 +261,9 @@ module.exports = function(Self) { removeUnloggable(definition, oldInstance); removeUnloggable(definition, newInstance); + oldInstance = await fkToValue(oldInstance, ctx); + newInstance = await fkToValue(newInstance, ctx); + // Prevent log with no new changes const hasNewChanges = Object.keys(newInstance).length; if (!hasNewChanges) return; diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 65028a8a8..3360a14fa 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -93,5 +93,7 @@ "New ticket request has been created": "New ticket request has been created *'{{description}}'* for day *{{shipped}}*, with a quantity of *{{quantity}}*", "There's a new urgent ticket": "There's a new urgent ticket: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})", "Swift / BIC cannot be empty": "Swift / BIC cannot be empty", - "Role name must be written in camelCase": "Role name must be written in camelCase" + "Role name must be written in camelCase": "Role name must be written in camelCase", + "None": "None", + "Client assignment has changed": "Client assignment has changed" } \ No newline at end of file diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 497fad00a..e1de363b2 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -226,11 +226,6 @@ module.exports = Self => { }; await Self.app.models.ClientCredit.create(newCredit); } - - if (orgData) - ctx.hookState.oldInstance = Object.assign({}, orgData.__data); - if (finalState) - ctx.hookState.newInstance = Object.assign({}, finalState); }); Self.observe('after save', async ctx => { From 2c1b15512212b2624d4adf0f094b4c47022e9687 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 24 Mar 2021 13:48:14 +0100 Subject: [PATCH 4/5] Added missing translation --- loopback/locale/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 3360a14fa..c147e6c10 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -94,6 +94,6 @@ "There's a new urgent ticket": "There's a new urgent ticket: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})", "Swift / BIC cannot be empty": "Swift / BIC cannot be empty", "Role name must be written in camelCase": "Role name must be written in camelCase", - "None": "None", - "Client assignment has changed": "Client assignment has changed" + "Client assignment has changed": "I did change the salesperson ~*\"<{{previousWorkerName}}>\"*~ by *\"<{{currentWorkerName}}>\"* from the client [{{clientName}} ({{clientId}})]({{{url}}})", + "None": "None" } \ No newline at end of file From 302dedef0f4ad4cc5ee0dbfd60b1119c757a2678 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 24 Mar 2021 14:25:57 +0100 Subject: [PATCH 5/5] Missing translations & attachment fix --- loopback/locale/es.json | 4 ++-- .../components/attachment/attachment.html | 2 +- .../core/components/attachment/attachment.js | 2 +- .../email/letter-debtor-st/locale/fr.yml | 20 +++++++++++++++++++ .../reports/letter-debtor/locale/fr.yml | 10 ++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 print/templates/email/letter-debtor-st/locale/fr.yml create mode 100644 print/templates/reports/letter-debtor/locale/fr.yml diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 96858eb4a..9c80bb816 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -167,8 +167,8 @@ "Action not allowed on the test environment": "Esta acción no está permitida en el entorno de pruebas", "The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta", "Sorts whole route": "Reordena ruta entera", - "New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}} y un precio de {{price}} €", - "New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}}", + "New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día *{{shipped}}*, con una cantidad de *{{quantity}}* y un precio de *{{price}} €*", + "New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día *{{shipped}}*, con una cantidad de *{{quantity}}*", "Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío", "This BIC already exist.": "Este BIC ya existe.", "That item doesn't exists": "Ese artículo no existe", diff --git a/print/core/components/attachment/attachment.html b/print/core/components/attachment/attachment.html index d66746e99..88fa64434 100644 --- a/print/core/components/attachment/attachment.html +++ b/print/core/components/attachment/attachment.html @@ -1,5 +1,5 @@
- +
{{attachment.filename}}
diff --git a/print/core/components/attachment/attachment.js b/print/core/components/attachment/attachment.js index 0f2a6d103..5c78a895c 100755 --- a/print/core/components/attachment/attachment.js +++ b/print/core/components/attachment/attachment.js @@ -1,7 +1,7 @@ module.exports = { name: 'attachment', computed: { - path() { + attachmentPath() { const filename = this.attachment.filename; const component = this.attachment.component; if (this.attachment.cid) diff --git a/print/templates/email/letter-debtor-st/locale/fr.yml b/print/templates/email/letter-debtor-st/locale/fr.yml new file mode 100644 index 000000000..a8da98a80 --- /dev/null +++ b/print/templates/email/letter-debtor-st/locale/fr.yml @@ -0,0 +1,20 @@ +subject: Avis initial de solde débiteur +title: Avis initial de solde débiteur +sections: + introduction: + title: Madame, Monsieur, + description: Sauf erreur ou omission de notre part, nous constatons que votre + compte client présente à ce jour un solde débiteur. +checkExtract: Ce montant correspond à nos factures restées impayées, ci-joint en annexe. + Notre service administratif se fera un plaisir de clarifier toutes les questions que vous + pourriez avoir, et vous fournira également tout document que vous nous demandez. +checkValidData: Si lors de la vérification des données fournies, elles sont correctes et + l’échéance étant dépassée, nous vous demandons de bien vouloir régulariser cette situation. +payMethod: Si vous ne souhaitez pas vous rendre personnellement à nos bureaux, vous + pouvez effectuer le paiement par virement bancaire sur le compte qui apparaît en + bas du relevé, en indiquant votre numéro de client, ou vous pouvez effectuer le + paiement en ligne avec une carte bleue sur notre site Internet. +conclusion: Dans le cas où votre règlement aurait été adressé entre temps, + nous vous prions de ne pas tenir compte de la présente. + Nous vous remercions par avance de votre aimable coopération. +transferAccount: Coordonées pour virement bancaire diff --git a/print/templates/reports/letter-debtor/locale/fr.yml b/print/templates/reports/letter-debtor/locale/fr.yml new file mode 100644 index 000000000..eff39d783 --- /dev/null +++ b/print/templates/reports/letter-debtor/locale/fr.yml @@ -0,0 +1,10 @@ +title: Relevé de compte +claimId: Réclamation +clientId: Client +clientData: Données client +date: Date +concept: Objet +invoiced: Facturé +payed: Payé +balance: Solde +client: Client {0} \ No newline at end of file