diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ee05fe4..a346591d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2314.01] - 2023-04-20 ### Added +- (Clientes -> Morosos) Ahora se puede filtrar por las columnas "Desde" y "Fecha Ú. O.". También se envia un email al comercial cuando se añade una nota. +- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia - (Facturas recibidas -> Bases negativas) Nueva sección ### Changed - ### Fixed -- +- (Clientes -> Morosos) Ahora se mantienen los elementos seleccionados al hacer sroll. ## [2312.01] - 2023-04-06 diff --git a/db/changes/231201/00-mailACL.sql b/db/changes/231201/00-mailACL.sql new file mode 100644 index 000000000..ac687818d --- /dev/null +++ b/db/changes/231201/00-mailACL.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) +VALUES('Mail', '*', '*', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/231401/00-clientBeforeUpdate.sql b/db/changes/231401/00-clientBeforeUpdate.sql new file mode 100644 index 000000000..8f9f70dd5 --- /dev/null +++ b/db/changes/231401/00-clientBeforeUpdate.sql @@ -0,0 +1,72 @@ +DROP TRIGGER IF EXISTS `vn`.`client_beforeUpdate`; +USE `vn`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`client_beforeUpdate` + BEFORE UPDATE ON `client` + FOR EACH ROW +BEGIN + DECLARE vText VARCHAR(255) DEFAULT NULL; + DECLARE vPayMethodFk INT; + -- Comprueba que el formato de los teléfonos es válido + + IF !(NEW.phone <=> OLD.phone) AND (NEW.phone <> '') THEN + CALL pbx.phone_isValid(NEW.phone); + END IF; + + IF !(NEW.mobile <=> OLD.mobile) AND (NEW.mobile <> '')THEN + CALL pbx.phone_isValid(NEW.mobile); + END IF; + + SELECT id INTO vPayMethodFk + FROM vn.payMethod + WHERE code = 'bankDraft'; + + IF NEW.payMethodFk = vPayMethodFk AND NEW.dueDay = 0 THEN + SET NEW.dueDay = 5; + END IF; + + -- Avisar al comercial si ha llegado la documentación sepa/core + + IF NEW.hasSepaVnl AND !OLD.hasSepaVnl THEN + SET vText = 'Sepa de VNL'; + END IF; + + IF NEW.hasCoreVnl AND !OLD.hasCoreVnl THEN + SET vText = 'Core de VNL'; + END IF; + + IF vText IS NOT NULL + THEN + INSERT INTO mail(receiver, replyTo, `subject`, body) + SELECT + CONCAT(IF(ac.id,u.name, 'jgallego'), '@verdnatura.es'), + 'administracion@verdnatura.es', + CONCAT('Cliente ', NEW.id), + CONCAT('Recibida la documentación: ', vText) + FROM worker w + LEFT JOIN account.user u ON w.userFk = u.id AND u.active + LEFT JOIN account.account ac ON ac.id = u.id + WHERE w.id = NEW.salesPersonFk; + END IF; + + IF NEW.salespersonFk IS NULL AND OLD.salespersonFk IS NOT NULL THEN + IF (SELECT COUNT(clientFk) + FROM clientProtected + WHERE clientFk = NEW.id + ) > 0 THEN + CALL util.throw("HAS_CLIENT_PROTECTED"); + END IF; + END IF; + + IF !(NEW.salesPersonFk <=> OLD.salesPersonFk) THEN + SET NEW.lastSalesPersonFk = IFNULL(NEW.salesPersonFk, OLD.salesPersonFk); + END IF; + + IF !(NEW.businessTypeFk <=> OLD.businessTypeFk) AND (NEW.businessTypeFk = 'individual' OR OLD.businessTypeFk = 'individual') THEN + SET NEW.isTaxDataChecked = 0; + END IF; + +END$$ +DELIMITER ; diff --git a/db/changes/231401/00-invoiceOutAfterInsert.sql b/db/changes/231401/00-invoiceOutAfterInsert.sql new file mode 100644 index 000000000..adeaf9834 --- /dev/null +++ b/db/changes/231401/00-invoiceOutAfterInsert.sql @@ -0,0 +1,13 @@ +DROP TRIGGER IF EXISTS `vn`.`invoiceOut_afterInsert`; +USE vn; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`invoiceOut_afterInsert` + AFTER INSERT ON `invoiceOut` + FOR EACH ROW +BEGIN + CALL clientRisk_update(NEW.clientFk, NEW.companyFk, NEW.amount); +END$$ +DELIMITER ; + diff --git a/loopback/locale/es.json b/loopback/locale/es.json index d6588c0b2..42276efe7 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -268,9 +268,11 @@ "Exists an invoice with a previous date": "Existe una factura con fecha anterior", "Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite", "Warehouse inventory not set": "El almacén inventario no está establecido", - "This locker has already been assigned": "Esta taquilla ya ha sido asignada", + "This locker has already been assigned": "Esta taquilla ya ha sido asignada", "Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}", "Not exist this branch": "La rama no existe", - "This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado", - "Insert a date range": "Inserte un rango de fechas" + "This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado", + "Insert a date range": "Inserte un rango de fechas", + "Added observation": "{{user}} añadió esta observacion: {{text}}", + "Comment added to client": "Observación añadida al cliente {{clientFk}}" } diff --git a/modules/client/back/methods/client/checkDuplicated.js b/modules/client/back/methods/client/checkDuplicated.js deleted file mode 100644 index 522cd088f..000000000 --- a/modules/client/back/methods/client/checkDuplicated.js +++ /dev/null @@ -1,63 +0,0 @@ -module.exports = Self => { - Self.remoteMethod('checkDuplicatedData', { - description: 'Checks if a client has same email, mobile or phone than other client and send an email', - accepts: [{ - arg: 'id', - type: 'number', - required: true, - description: 'The client id' - }], - returns: { - type: 'object', - root: true - }, - http: { - verb: 'GET', - path: '/:id/checkDuplicatedData' - } - }); - - Self.checkDuplicatedData = async function(id, options) { - const myOptions = {}; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - const client = await Self.app.models.Client.findById(id, myOptions); - - const findParams = []; - if (client.email) { - const emails = client.email.split(','); - for (let email of emails) - findParams.push({email: email}); - } - - if (client.phone) - findParams.push({phone: client.phone}); - - if (client.mobile) - findParams.push({mobile: client.mobile}); - - const filterObj = { - where: { - and: [ - {or: findParams}, - {id: {neq: client.id}} - ] - } - }; - - const clientSameData = await Self.findOne(filterObj, myOptions); - - if (clientSameData) { - await Self.app.models.Mail.create({ - receiver: 'direccioncomercial@verdnatura.es', - subject: `Cliente con email/teléfono/móvil duplicados`, - body: 'El cliente ' + client.id + ' comparte alguno de estos datos con el cliente ' + clientSameData.id + - '\n- Email: ' + client.email + - '\n- Teléfono: ' + client.phone + - '\n- Móvil: ' + client.mobile - }, myOptions); - } - }; -}; diff --git a/modules/client/back/methods/client/specs/checkDuplicated.spec.js b/modules/client/back/methods/client/specs/checkDuplicated.spec.js deleted file mode 100644 index 1b682ca35..000000000 --- a/modules/client/back/methods/client/specs/checkDuplicated.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -const models = require('vn-loopback/server/server').models; - -describe('client checkDuplicated()', () => { - it('should send an mail if mobile/phone/email is duplicated', async() => { - const tx = await models.Client.beginTransaction({}); - - try { - const options = {transaction: tx}; - - const id = 1110; - const mailModel = models.Mail; - spyOn(mailModel, 'create'); - - await models.Client.checkDuplicatedData(id, options); - - expect(mailModel.create).toHaveBeenCalled(); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); diff --git a/modules/client/back/methods/defaulter/observationEmail.js b/modules/client/back/methods/defaulter/observationEmail.js new file mode 100644 index 000000000..c3c96010e --- /dev/null +++ b/modules/client/back/methods/defaulter/observationEmail.js @@ -0,0 +1,52 @@ +module.exports = Self => { + Self.remoteMethodCtx('observationEmail', { + description: 'Send an email with the observation', + accessType: 'WRITE', + accepts: [ + { + arg: 'defaulters', + type: ['object'], + required: true, + description: 'The defaulters to send the email' + }, + { + arg: 'observation', + type: 'string', + required: true, + description: 'The observation' + }], + returns: { + arg: 'observationEmail' + }, + http: { + path: `/observationEmail`, + verb: 'POST' + } + }); + + Self.observationEmail = async(ctx, defaulters, observation, options) => { + const models = Self.app.models; + const $t = ctx.req.__; // $translate + const myOptions = {}; + const userId = ctx.req.accessToken.userId; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + for (const defaulter of defaulters) { + const user = await models.Account.findById(userId, {fields: ['name']}, myOptions); + + const body = $t('Added observation', { + user: user.name, + text: observation + }); + + await models.Mail.create({ + subject: $t('Comment added to client', {clientFk: defaulter.clientFk}), + body: body, + receiver: `${defaulter.salesPersonName}@verdnatura.es`, + replyTo: `${user.name}@verdnatura.es` + }, myOptions); + } + }; +}; diff --git a/modules/client/back/models/client-methods.js b/modules/client/back/models/client-methods.js index fc77fc090..3538dbeb8 100644 --- a/modules/client/back/models/client-methods.js +++ b/modules/client/back/models/client-methods.js @@ -2,7 +2,6 @@ module.exports = Self => { require('../methods/client/addressesPropagateRe')(Self); require('../methods/client/canBeInvoiced')(Self); require('../methods/client/canCreateTicket')(Self); - require('../methods/client/checkDuplicated')(Self); require('../methods/client/confirmTransaction')(Self); require('../methods/client/consumption')(Self); require('../methods/client/createAddress')(Self); diff --git a/modules/client/back/models/defaulter.js b/modules/client/back/models/defaulter.js index 13bb1a614..868d6cd0a 100644 --- a/modules/client/back/models/defaulter.js +++ b/modules/client/back/models/defaulter.js @@ -1,3 +1,4 @@ module.exports = Self => { require('../methods/defaulter/filter')(Self); + require('../methods/defaulter/observationEmail')(Self); }; diff --git a/modules/client/front/basic-data/index.js b/modules/client/front/basic-data/index.js index b08d642d1..ed34eefc4 100644 --- a/modules/client/front/basic-data/index.js +++ b/modules/client/front/basic-data/index.js @@ -9,9 +9,7 @@ export default class Controller extends Section { } onSubmit() { - return this.$.watcher.submit().then(() => { - this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`); - }); + return this.$.watcher.submit(); } } diff --git a/modules/client/front/create/index.js b/modules/client/front/create/index.js index 9ca58ed10..631029802 100644 --- a/modules/client/front/create/index.js +++ b/modules/client/front/create/index.js @@ -12,7 +12,6 @@ export default class Controller extends Section { onSubmit() { return this.$.watcher.submit().then(json => { this.$state.go('client.card.basicData', {id: json.data.id}); - this.$http.get(`Clients/${this.client.id}/checkDuplicatedData`); }); } diff --git a/modules/client/front/defaulter/index.html b/modules/client/front/defaulter/index.html index 22b78594a..8f22629a9 100644 --- a/modules/client/front/defaulter/index.html +++ b/modules/client/front/defaulter/index.html @@ -5,6 +5,7 @@ limit="20" order="amount DESC" data="defaulters" + on-data-change="$ctrl.reCheck()" auto-load="true"> @@ -17,22 +18,22 @@ -
Total
-
- - @@ -56,25 +57,25 @@ Comercial - Balance D. - Author Last observation - L. O. Date - @@ -88,8 +89,9 @@ - @@ -150,7 +152,7 @@ - + @@ -160,7 +162,7 @@ id !== clientId) : [...this.checkedDefaulers, clientId]; + } + + reCheck() { + if (!this.$.model.data || !this.checkedDefaulers.length) return; + + this.$.model.data.forEach(defaulter => { + defaulter.checked = this.checkedDefaulers.includes(defaulter.clientFk); + }); + } + getBalanceDueTotal() { this.$http.get('Defaulters/filter') .then(res => { @@ -109,11 +123,20 @@ export default class Controller extends Section { } this.$http.post(`ClientObservations`, params) .then(() => { - this.vnApp.showMessage(this.$t('Observation saved!')); + this.vnApp.showSuccess(this.$t('Observation saved!')); + this.sendMail(); this.$state.reload(); }); } + sendMail() { + const params = { + defaulters: this.checked, + observation: this.defaulter.observation + }; + this.$http.post(`Defaulters/observationEmail`, params); + } + exprBuilder(param, value) { switch (param) { case 'creditInsurance': @@ -122,8 +145,25 @@ export default class Controller extends Section { case 'workerFk': case 'salesPersonFk': return {[`d.${param}`]: value}; + case 'created': + return {'d.created': { + between: this.dateRange(value)} + }; + case 'defaulterSinced': + return {'d.defaulterSinced': { + between: this.dateRange(value)} + }; } } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } } ngModule.vnComponent('vnClientDefaulter', { diff --git a/modules/client/front/defaulter/index.spec.js b/modules/client/front/defaulter/index.spec.js index f92378d08..b4a9df184 100644 --- a/modules/client/front/defaulter/index.spec.js +++ b/modules/client/front/defaulter/index.spec.js @@ -81,14 +81,15 @@ describe('client defaulter', () => { const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}]; - jest.spyOn(controller.vnApp, 'showMessage'); + jest.spyOn(controller.vnApp, 'showSuccess'); $httpBackend.expect('GET', `Defaulters/filter`).respond(200); $httpBackend.expect('POST', `ClientObservations`, params).respond(200, params); + $httpBackend.expect('POST', `Defaulters/observationEmail`).respond(200); controller.onResponse(); $httpBackend.flush(); - expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Observation saved!'); + expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Observation saved!'); }); }); @@ -117,5 +118,62 @@ describe('client defaulter', () => { expect(controller.balanceDueTotal).toEqual(875); }); }); + + describe('dateRange()', () => { + it('should return two dates with the hours at the start and end of the given date', () => { + const now = Date.vnNew(); + + const today = now.getDate(); + + const dateRange = controller.dateRange(now); + const start = dateRange[0].toString(); + const end = dateRange[1].toString(); + + expect(start).toContain(today); + expect(start).toContain('00:00:00'); + + expect(end).toContain(today); + expect(end).toContain('23:59:59'); + }); + }); + + describe('reCheck()', () => { + it(`should recheck buys`, () => { + controller.$.model.data = [ + {checked: false, clientFk: 1}, + {checked: false, clientFk: 2}, + {checked: false, clientFk: 3}, + {checked: false, clientFk: 4}, + ]; + controller.checkedDefaulers = [1, 2]; + + controller.reCheck(); + + expect(controller.$.model.data[0].checked).toEqual(true); + expect(controller.$.model.data[1].checked).toEqual(true); + expect(controller.$.model.data[2].checked).toEqual(false); + expect(controller.$.model.data[3].checked).toEqual(false); + }); + }); + + describe('saveChecked()', () => { + it(`should check buy`, () => { + const buyCheck = 3; + controller.checkedDefaulers = [1, 2]; + + controller.saveChecked(buyCheck); + + expect(controller.checkedDefaulers[2]).toEqual(buyCheck); + }); + + it(`should uncheck buy`, () => { + const buyUncheck = 3; + controller.checkedDefaulers = [1, 2, 3]; + + controller.saveChecked(buyUncheck); + + expect(controller.checkedDefaulers[2]).toEqual(undefined); + }); + }); }); }); diff --git a/modules/client/front/defaulter/locale/es.yml b/modules/client/front/defaulter/locale/es.yml index c3e1d4e19..fe06a15a1 100644 --- a/modules/client/front/defaulter/locale/es.yml +++ b/modules/client/front/defaulter/locale/es.yml @@ -6,4 +6,6 @@ Last observation: Última observación L. O. Date: Fecha Ú. O. Last observation date: Fecha última observación Search client: Buscar clientes -Worker who made the last observation: Trabajador que ha realizado la última observación \ No newline at end of file +Worker who made the last observation: Trabajador que ha realizado la última observación +Email sended!: Email enviado! +Observation saved!: Observación añadida! diff --git a/modules/client/front/fiscal-data/index.js b/modules/client/front/fiscal-data/index.js index d76944c42..acad38185 100644 --- a/modules/client/front/fiscal-data/index.js +++ b/modules/client/front/fiscal-data/index.js @@ -2,6 +2,10 @@ import ngModule from '../module'; import Section from 'salix/components/section'; export default class Controller extends Section { + $onInit() { + this.card.reload(); + } + onSubmit() { const orgData = this.$.watcher.orgData; delete this.client.despiteOfClient; diff --git a/modules/item/front/request-search-panel/index.html b/modules/item/front/request-search-panel/index.html index 8c9d04b64..a431d4fd6 100644 --- a/modules/item/front/request-search-panel/index.html +++ b/modules/item/front/request-search-panel/index.html @@ -38,6 +38,19 @@ url="Warehouses"> + + + {{firstName}} {{lastName}} + + +
{ type: 'number', description: `Search requests attended by a given worker id` }, + { + arg: 'requesterFk', + type: 'number' + }, { arg: 'mine', type: 'boolean', @@ -89,6 +93,8 @@ module.exports = Self => { return {'t.id': value}; case 'attenderFk': return {'tr.attenderFk': value}; + case 'requesterFk': + return {'tr.requesterFk': value}; case 'state': switch (value) { case 'pending': @@ -125,6 +131,7 @@ module.exports = Self => { tr.description, tr.response, tr.saleFk, + tr.requesterFk, tr.isOk, s.quantity AS saleQuantity, s.itemFk,