From 146b4f5bcdc350a3da10445c122d43b80e954e58 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 28 Oct 2020 14:12:57 +0100 Subject: [PATCH 01/12] 2534 - Fill sage data before enable taxDataChecked --- .../12_lock_of_verified_data.spec.js | 2 ++ gulpfile.js | 1 + loopback/locale/en.json | 3 ++- loopback/locale/es.json | 3 ++- .../client/specs/updateFiscalData.spec.js | 26 ++++++++++++++++--- .../back/methods/client/updateFiscalData.js | 15 ++++++++--- modules/client/back/models/client.js | 2 +- modules/client/front/fiscal-data/index.html | 8 ++++-- 8 files changed, 49 insertions(+), 11 deletions(-) diff --git a/e2e/paths/02-client/12_lock_of_verified_data.spec.js b/e2e/paths/02-client/12_lock_of_verified_data.spec.js index b90ab7dfc..5d457219e 100644 --- a/e2e/paths/02-client/12_lock_of_verified_data.spec.js +++ b/e2e/paths/02-client/12_lock_of_verified_data.spec.js @@ -57,6 +57,8 @@ describe('Client lock verified data path', () => { }); it('should check the Verified data checkbox', async() => { + await page.autocompleteSearch(selectors.clientFiscalData.sageTax, 'operaciones no sujetas'); + await page.autocompleteSearch(selectors.clientFiscalData.sageTransaction, 'regularización de inversiones'); await page.waitToClick(selectors.clientFiscalData.verifiedDataCheckbox); await page.waitToClick(selectors.clientFiscalData.saveButton); await page.waitToClick(selectors.globalItems.acceptButton); diff --git a/gulpfile.js b/gulpfile.js index 6bd2b3542..67d261d73 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,7 @@ let buildDir = 'dist'; let backSources = [ '!node_modules', + '!loopback/locale/*.json', 'loopback', 'modules/*/back/**', 'modules/*/back/*', diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 3dfb73833..172da6faf 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -81,5 +81,6 @@ "shipped": "Shipped", "landed": "Landed", "addressFk": "Address", - "companyFk": "Company" + "companyFk": "Company", + "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index c3fe287eb..7cd362ad8 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -156,5 +156,6 @@ "shipped": "F. envío", "landed": "F. entrega", "addressFk": "Consignatario", - "companyFk": "Empresa" + "companyFk": "Empresa", + "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data" } \ No newline at end of file diff --git a/modules/client/back/methods/client/specs/updateFiscalData.spec.js b/modules/client/back/methods/client/specs/updateFiscalData.spec.js index 288183fb6..9c73e29ec 100644 --- a/modules/client/back/methods/client/specs/updateFiscalData.spec.js +++ b/modules/client/back/methods/client/specs/updateFiscalData.spec.js @@ -2,8 +2,9 @@ const app = require('vn-loopback/server/server'); describe('Client updateFiscalData', () => { const clientId = 101; + const salesAssistantId = 21; + const administrativeId = 5; afterAll(async done => { - const clientId = 101; const ctx = {req: {accessToken: {userId: 5}}}; ctx.args = {postcode: 46460}; @@ -22,11 +23,30 @@ describe('Client updateFiscalData', () => { error = e; }); - expect(error.message).toBeDefined(); + expect(error.message).toEqual(`You can't make changes on a client with verified data`); + }); + + it('should return an error if the user is not d and the isTaxDataChecked value is true', async() => { + const client = await app.models.Client.findById(clientId); + await client.updateAttribute('isTaxDataChecked', false); + + const ctx = {req: {accessToken: {userId: salesAssistantId}}}; + ctx.args = {isTaxDataChecked: true}; + + let error; + await app.models.Client.updateFiscalData(ctx, clientId) + .catch(e => { + error = e; + }); + + expect(error.message).toEqual('You need to fill sage information before you check verified data'); + + // Restores + await client.updateAttribute('isTaxDataChecked', true); }); it('should update the client fiscal data and return the count if changes made', async() => { - const ctx = {req: {accessToken: {userId: 5}}}; + const ctx = {req: {accessToken: {userId: administrativeId}}}; ctx.args = {postcode: 46680}; const client = await app.models.Client.findById(clientId); diff --git a/modules/client/back/methods/client/updateFiscalData.js b/modules/client/back/methods/client/updateFiscalData.js index 48cc6df88..a3d40a4d8 100644 --- a/modules/client/back/methods/client/updateFiscalData.js +++ b/modules/client/back/methods/client/updateFiscalData.js @@ -45,15 +45,15 @@ module.exports = Self => { }, { arg: 'sageTaxTypeFk', - type: 'number' + type: 'any' }, { arg: 'sageTransactionTypeFk', - type: 'number' + type: 'any' }, { arg: 'transferorFk', - type: 'number' + type: 'any' }, { arg: 'hasToInvoiceByAddress', @@ -118,6 +118,15 @@ module.exports = Self => { if (!isSalesAssistant && client.isTaxDataChecked) throw new UserError(`You can't make changes on a client with verified data`); + // Sage data validation + const taxDataChecked = args.isTaxDataChecked; + const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk; + const sageTransactionChecked = client.sageTransactionTypeFk || args.sageTransactionTypeFk; + const hasSageData = sageTaxChecked && sageTransactionChecked; + + if (taxDataChecked && !hasSageData) + throw new UserError(`You need to fill sage information before you check verified data`); + if (args.despiteOfClient) { const logRecord = { originFk: clientId, diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 36bc60dfa..6723e865a 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -187,7 +187,7 @@ module.exports = Self => { // Validate socialName format const hasChanges = orgData && changes; - const socialName = changes.socialName || orgData.socialName; + const socialName = changes && changes.socialName || orgData && orgData.socialName; const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); const socialNameChanged = hasChanges diff --git a/modules/client/front/fiscal-data/index.html b/modules/client/front/fiscal-data/index.html index 83b82c9ea..cc9e53097 100644 --- a/modules/client/front/fiscal-data/index.html +++ b/modules/client/front/fiscal-data/index.html @@ -30,7 +30,7 @@ data="sageTransactionTypes" order="transaction"> -
+ + info="You can use letters and spaces" + required="true"> From 245b32a47f8244080bb446dfbbfea9161414e209 Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 30 Oct 2020 15:11:45 +0100 Subject: [PATCH 02/12] Added section --- back/model-config.json | 3 + back/models/sage-withholding.json | 33 +++ db/dump/dumpedFixtures.sql | 18 ++ db/dump/fixtures.sql | 8 +- .../methods/supplier/specs/getSummary.spec.js | 2 +- .../supplier/specs/updateFiscalData.spec.js | 80 +++++++ .../back/methods/supplier/updateFiscalData.js | 78 +++++++ modules/supplier/back/models/supplier.js | 1 + modules/supplier/back/models/supplier.json | 24 +- modules/supplier/front/fiscal-data/index.html | 177 ++++++++++++++ modules/supplier/front/fiscal-data/index.js | 84 +++++++ .../supplier/front/fiscal-data/index.spec.js | 215 ++++++++++++++++++ .../supplier/front/fiscal-data/locale/es.yml | 3 + modules/supplier/front/index.js | 2 + modules/supplier/front/routes.json | 18 ++ 15 files changed, 738 insertions(+), 8 deletions(-) create mode 100644 back/models/sage-withholding.json create mode 100644 modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js create mode 100644 modules/supplier/back/methods/supplier/updateFiscalData.js create mode 100644 modules/supplier/front/fiscal-data/index.html create mode 100644 modules/supplier/front/fiscal-data/index.js create mode 100644 modules/supplier/front/fiscal-data/index.spec.js create mode 100644 modules/supplier/front/fiscal-data/locale/es.yml diff --git a/back/model-config.json b/back/model-config.json index 7a59aaf9a..bab228cd5 100644 --- a/back/model-config.json +++ b/back/model-config.json @@ -56,6 +56,9 @@ "Sip": { "dataSource": "vn" }, + "SageWithholding": { + "dataSource": "vn" + }, "UserConfigView": { "dataSource": "vn" }, diff --git a/back/models/sage-withholding.json b/back/models/sage-withholding.json new file mode 100644 index 000000000..8d93daeae --- /dev/null +++ b/back/models/sage-withholding.json @@ -0,0 +1,33 @@ +{ + "name": "SageWithholding", + "base": "VnModel", + "options": { + "mysql": { + "table": "sage.TiposRetencion" + } + }, + "properties": { + "id": { + "type": "Number", + "id": true, + "description": "Identifier", + "mysql": { + "columnName": "CodigoRetencion" + } + }, + "withholding": { + "type": "string", + "mysql": { + "columnName": "Retencion" + } + } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} \ No newline at end of file diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql index 70e5d9b83..c87eab826 100644 --- a/db/dump/dumpedFixtures.sql +++ b/db/dump/dumpedFixtures.sql @@ -604,6 +604,24 @@ INSERT INTO `TiposTransacciones` VALUES (1,'Rég.general/Oper.interiores bienes UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; +-- +-- Dumping data for table `TiposRetencion` +-- + +LOCK TABLES `TiposRetencion` WRITE; +/*!40000 ALTER TABLE `TiposRetencion` DISABLE KEYS */; +INSERT INTO `TiposRetencion` (`CodigoRetencion`, `Retencion`, `PorcentajeRetencion`, `CuentaCargo`, `CuentaAbono`, `ClaveIrpf`, `CuentaCargoANT_`, `CuentaAbonoANT_`, `IdTipoRetencion`) VALUES +(1, 'RETENCION ESTIMACION OBJETIVA', '1.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, '03811652-0F3A-44A1-AE1C-B19624525D7F'), +(2, 'ACTIVIDADES AGRICOLAS O GANADERAS', '2.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, 'F3F91EF3-FED6-444D-B03C-75B639D13FB4'), +(9, 'ACTIVIDADES PROFESIONALES 2 PRIMEROS AÑOS', '9.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, '73F95642-E951-4C91-970A-60C503A4792B'), +(15, 'ACTIVIDADES PROFESIONALES', '15.0000000000', '4730000000', '4751000000', '6', NULL, NULL, 'F6BDE0EE-3B01-4023-8FFF-A73AE9AC50D7'), +(19, 'ARRENDAMIENTO Y SUBARRENDAMIENTO', '19.0000000000', '4730000000', '4751000000', '8', NULL, NULL, '09B033AE-16E5-4057-8D4A-A7710C8A4FB9'); +/*!40000 ALTER TABLE `TiposRetencion` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + + + /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index cc3f2f689..ed194e3a8 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -1208,11 +1208,11 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`) (104, 500), (105, 5000); -INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`retAccount`,`commission`, `created`, `postcodeFk`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`) +INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`retAccount`,`commission`, `created`, `postcodeFk`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`) VALUES - (1, 'Plants SL', 'Plants nick', 4000000001, 1, '06089160W', 0, NULL, 0, CURDATE(), 1111, 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15), - (2, 'Flower King', 'The king', 4000000002, 1, 'B22222222', 0, NULL, 0, CURDATE(), 2222, 1, 'supplier address 2', 'LONDON', 2, 45671, 1, 2, 10), - (442, 'Verdnatura Levante SL', 'Verdnatura', 4000000442, 1, 'C33333333', 0, NULL, 0, CURDATE(), 3333, 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15); + (1, 'Plants SL', 'Plants nick', 4000000001, 1, '06089160W', 0, NULL, 0, CURDATE(), 1111, 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1), + (2, 'Flower King', 'The king', 4000000002, 1, 'B22222222', 0, NULL, 0, CURDATE(), 2222, 1, 'supplier address 2', 'LONDON', 2, 45671, 1, 2, 10, 5, 2, 2), + (442, 'Verdnatura Levante SL', 'Verdnatura', 4000000442, 1, 'C33333333', 0, NULL, 0, CURDATE(), 3333, 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3); INSERT INTO `cache`.`cache_calc`(`id`, `cache_id`, `cacheName`, `params`, `last_refresh`, `expires`, `created`, `connection_id`) VALUES diff --git a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js index 42e89afd4..ec2e89c88 100644 --- a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js +++ b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js @@ -7,7 +7,7 @@ describe('Supplier getSummary()', () => { expect(supplier.id).toEqual(1); expect(supplier.name).toEqual('Plants SL'); expect(supplier.nif).toEqual('06089160W'); - expect(supplier.account).toEqual(4000000001); + expect(supplier.account).toEqual('4000000001'); expect(supplier.payDay).toEqual(15); }); diff --git a/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js new file mode 100644 index 000000000..eac9fe899 --- /dev/null +++ b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js @@ -0,0 +1,80 @@ +const app = require('vn-loopback/server/server'); + +describe('Supplier updateFiscalData', () => { + const supplierId = 1; + const administrativeId = 5; + const employeeId = 1; + const defaultData = { + name: 'Plants SL', + nif: '06089160W', + account: '4000000001', + sageTaxTypeFk: 4, + sageWithholdingFk: 1, + sageTransactionTypeFk: 1, + postCode: '15214', + city: 'PONTEVEDRA', + provinceFk: 1, + countryFk: 1, + }; + + it('should return an error if the user is not administrative', async() => { + const ctx = {req: {accessToken: {userId: employeeId}}}; + ctx.args = {}; + + let error; + await app.models.Supplier.updateFiscalData(ctx, supplierId) + .catch(e => { + error = e; + }); + + expect(error.message).toBeDefined(); + }); + + it('should check that the supplier fiscal data is untainted', async() => { + const supplier = await app.models.Supplier.findById(supplierId); + + expect(supplier.name).toEqual(defaultData.name); + expect(supplier.nif).toEqual(defaultData.nif); + expect(supplier.account).toEqual(defaultData.account); + expect(supplier.sageTaxTypeFk).toEqual(defaultData.sageTaxTypeFk); + expect(supplier.sageWithholdingFk).toEqual(defaultData.sageWithholdingFk); + expect(supplier.sageTransactionTypeFk).toEqual(defaultData.sageTransactionTypeFk); + expect(supplier.postCode).toEqual(defaultData.postCode); + expect(supplier.city).toEqual(defaultData.city); + expect(supplier.provinceFk).toEqual(defaultData.provinceFk); + expect(supplier.countryFk).toEqual(defaultData.countryFk); + }); + + it('should update the supplier fiscal data and return the count if changes made', async() => { + const ctx = {req: {accessToken: {userId: administrativeId}}}; + ctx.args = { + name: 'Weapon Dealer', + nif: 'B11111111', + account: '4000000005', + sageTaxTypeFk: 5, + sageWithholdingFk: 2, + sageTransactionTypeFk: 2, + postCode: '46460', + city: 'VALENCIA', + provinceFk: 2, + countryFk: 2, + }; + + const result = await app.models.Supplier.updateFiscalData(ctx, supplierId); + + expect(result.name).toEqual('Weapon Dealer'); + expect(result.nif).toEqual('B11111111'); + expect(result.account).toEqual('4000000005'); + expect(result.sageTaxTypeFk).toEqual(5); + expect(result.sageWithholdingFk).toEqual(2); + expect(result.sageTransactionTypeFk).toEqual(2); + expect(result.postCode).toEqual('46460'); + expect(result.city).toEqual('VALENCIA'); + expect(result.provinceFk).toEqual(2); + expect(result.countryFk).toEqual(2); + + // Restores + ctx.args = defaultData; + await app.models.Supplier.updateFiscalData(ctx, supplierId); + }); +}); diff --git a/modules/supplier/back/methods/supplier/updateFiscalData.js b/modules/supplier/back/methods/supplier/updateFiscalData.js new file mode 100644 index 000000000..be031a18a --- /dev/null +++ b/modules/supplier/back/methods/supplier/updateFiscalData.js @@ -0,0 +1,78 @@ +module.exports = Self => { + Self.remoteMethod('updateFiscalData', { + description: 'Updates fiscal data of a supplier', + accessType: 'WRITE', + accepts: [{ + arg: 'ctx', + type: 'Object', + http: {source: 'context'} + }, + { + arg: 'id', + type: 'Number', + description: 'The supplier id', + http: {source: 'path'} + }, + { + arg: 'name', + type: 'string' + }, + { + arg: 'nif', + type: 'string' + }, + { + arg: 'account', + type: 'string' + }, + { + arg: 'sageTaxTypeFk', + type: 'number' + }, + { + arg: 'sageWithholdingFk', + type: 'number' + }, + { + arg: 'sageTransactionTypeFk', + type: 'number' + }, + { + arg: 'postCode', + type: 'string' + }, + { + arg: 'city', + type: 'string' + }, + { + arg: 'provinceFk', + type: 'number' + }, + { + arg: 'countryFk', + type: 'number' + }], + returns: { + arg: 'res', + type: 'string', + root: true + }, + http: { + path: `/:id/updateFiscalData`, + verb: 'PATCH' + } + }); + + Self.updateFiscalData = async(ctx, supplierId) => { + const models = Self.app.models; + const args = ctx.args; + const supplier = await models.Supplier.findById(supplierId); + + // Remove unwanted properties + delete args.ctx; + delete args.id; + + return supplier.updateAttributes(args); + }; +}; diff --git a/modules/supplier/back/models/supplier.js b/modules/supplier/back/models/supplier.js index d3c32b814..d042037e7 100644 --- a/modules/supplier/back/models/supplier.js +++ b/modules/supplier/back/models/supplier.js @@ -1,4 +1,5 @@ module.exports = Self => { require('../methods/supplier/filter')(Self); require('../methods/supplier/getSummary')(Self); + require('../methods/supplier/updateFiscalData')(Self); }; diff --git a/modules/supplier/back/models/supplier.json b/modules/supplier/back/models/supplier.json index b4f5b5318..f0bcd7df8 100644 --- a/modules/supplier/back/models/supplier.json +++ b/modules/supplier/back/models/supplier.json @@ -16,7 +16,7 @@ "type": "String" }, "account": { - "type": "Number" + "type": "String" }, "countryFk": { "type": "Number" @@ -61,7 +61,7 @@ "type": "Number" }, "postCode": { - "type": "Number" + "type": "String" }, "payMethodFk": { "type": "Number" @@ -74,7 +74,25 @@ }, "nickname": { "type": "String" - } + }, + "sageTaxTypeFk": { + "type": "number", + "mysql": { + "columnName": "taxTypeSageFk" + } + }, + "sageTransactionTypeFk": { + "type": "number", + "mysql": { + "columnName": "transactionTypeSageFk" + } + }, + "sageWithholdingFk": { + "type": "number", + "mysql": { + "columnName": "withholdingSageFk" + } + } }, "relations": { "payMethod": { diff --git a/modules/supplier/front/fiscal-data/index.html b/modules/supplier/front/fiscal-data/index.html new file mode 100644 index 000000000..32fd38ad4 --- /dev/null +++ b/modules/supplier/front/fiscal-data/index.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{code}} - {{town.name}} ({{town.province.name}}, + {{town.province.country.country}}) + + + + + + + + + {{name}}, {{province.name}} + ({{province.country.country}}) + + + + + + {{name}} ({{country.country}}) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/supplier/front/fiscal-data/index.js b/modules/supplier/front/fiscal-data/index.js new file mode 100644 index 000000000..56588247b --- /dev/null +++ b/modules/supplier/front/fiscal-data/index.js @@ -0,0 +1,84 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + get province() { + return this._province; + } + + // Province auto complete + set province(selection) { + this._province = selection; + + if (!selection) return; + + const country = selection.country; + + if (!this.supplier.countryFk) + this.supplier.countryFk = country.id; + } + + get town() { + return this._town; + } + + // Town auto complete + set town(selection) { + this._town = selection; + + if (!selection) return; + + const province = selection.province; + const country = province.country; + const postcodes = selection.postcodes; + + if (!this.supplier.provinceFk) + this.supplier.provinceFk = province.id; + + if (!this.supplier.countryFk) + this.supplier.countryFk = country.id; + + if (postcodes.length === 1) + this.supplier.postCode = postcodes[0].code; + } + + get postcode() { + return this._postcode; + } + + // Postcode auto complete + set postcode(selection) { + const oldValue = this._postcode; + this._postcode = selection; + + if (!selection || !oldValue) return; + + const town = selection.town; + const province = town.province; + const country = province.country; + + if (!this.supplier.city) + this.supplier.city = town.name; + + if (!this.supplier.provinceFk) + this.supplier.provinceFk = province.id; + + if (!this.supplier.countryFk) + this.supplier.countryFk = country.id; + } + + onResponse(response) { + this.supplier.postCode = response.code; + this.supplier.city = response.city; + this.supplier.provinceFk = response.provinceFk; + this.supplier.countryFk = response.countryFk; + } +} + +ngModule.vnComponent('vnSupplierFiscalData', { + template: require('./index.html'), + controller: Controller, + bindings: { + supplier: '<' + } +}); diff --git a/modules/supplier/front/fiscal-data/index.spec.js b/modules/supplier/front/fiscal-data/index.spec.js new file mode 100644 index 000000000..b96de89ac --- /dev/null +++ b/modules/supplier/front/fiscal-data/index.spec.js @@ -0,0 +1,215 @@ +import './index'; +import watcher from 'core/mocks/watcher'; + +describe('Supplier', () => { + describe('Component vnSupplierFiscalData', () => { + let $httpBackend; + let $scope; + let $element; + let controller; + + beforeEach(ngModule('supplier')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + $scope = $rootScope.$new(); + $scope.watcher = watcher; + $scope.watcher.orgData = {id: 101, isEqualizated: false, isTaxDataChecked: false}; + $element = angular.element(''); + controller = $componentController('vnClientFiscalData', {$element, $scope}); + controller.card = {reload: () => {}}; + controller.client = { + id: 101, + email: 'batman@gothamcity.com', + phone: '1111111111', + isEqualizated: false, + isTaxDataChecked: false + }; + + controller._province = {}; + controller._town = {}; + controller._postcode = {}; + })); + + describe('onSubmit()', () => { + it('should call the save() method directly', () => { + jest.spyOn(controller, 'save'); + + controller.onSubmit(); + + expect(controller.save).toHaveBeenCalledWith(); + }); + + it('should call the checkExistingClient() if the isTaxDataChecked property is checked', () => { + jest.spyOn(controller, 'save'); + jest.spyOn(controller, 'checkExistingClient'); + + controller.client.isTaxDataChecked = true; + controller.onSubmit(); + + expect(controller.save).not.toHaveBeenCalledWith(); + expect(controller.checkExistingClient).toHaveBeenCalledWith(); + }); + }); + + describe('checkExistingClient()', () => { + it(`should make a HTTP GET query filtering by email, phone and mobile`, () => { + controller.client.mobile = 222222222; + const filterObj = { + where: { + and: [ + {or: [ + {email: controller.client.email}, + {phone: controller.client.phone}, + {mobile: controller.client.mobile} + ]}, + {id: {neq: controller.client.id}} + ] + } + }; + const filter = encodeURIComponent(JSON.stringify(filterObj)); + $httpBackend.expect('GET', `Clients/findOne?filter=${filter}`).respond(404); + controller.checkExistingClient(); + $httpBackend.flush(); + }); + + it(`should show a save confirmation and then set the despiteOfClient property`, () => { + controller.$.confirmDuplicatedClient = {show: () => {}}; + jest.spyOn(controller.$.confirmDuplicatedClient, 'show'); + + const filterObj = { + where: { + and: [ + {or: [{email: controller.client.email}, {phone: controller.client.phone}]}, + {id: {neq: controller.client.id}} + ] + } + }; + const expectedClient = {id: 102}; + const filter = encodeURIComponent(JSON.stringify(filterObj)); + $httpBackend.expect('GET', `Clients/findOne?filter=${filter}`).respond(expectedClient); + controller.checkExistingClient(); + $httpBackend.flush(); + + expect(controller.$.confirmDuplicatedClient.show).toHaveBeenCalledWith(); + expect(controller.client.despiteOfClient).toEqual(102); + }); + }); + + describe('checkEtChanges()', () => { + it(`should show a propagation confirmation if isEqualizated property is changed and invoice by address is checked`, () => { + controller.$.propagateIsEqualizated = {show: () => {}}; + jest.spyOn(controller.$.propagateIsEqualizated, 'show'); + + const orgData = $scope.watcher.orgData; + orgData.hasToInvoiceByAddress = true; + controller.client.isEqualizated = true; + + controller.checkEtChanges(orgData); + + expect(controller.$.propagateIsEqualizated.show).toHaveBeenCalledWith(); + }); + + it(`should call to the onAcceptEt() method if isEqualizated property is changed and invoice by address isn't checked`, () => { + jest.spyOn(controller, 'onAcceptEt'); + + const orgData = $scope.watcher.orgData; + orgData.hasToInvoiceByAddress = false; + controller.client.isEqualizated = true; + + controller.checkEtChanges(orgData); + + expect(controller.onAcceptEt).toHaveBeenCalledWith(); + }); + }); + + describe('onAcceptEt()', () => { + it('should request to patch the propagation of tax status', () => { + controller.client = {id: 123, isEqualizated: false}; + $httpBackend.expectPATCH(`Clients/${controller.client.id}/addressesPropagateRe`, {isEqualizated: controller.client.isEqualizated}).respond('done'); + controller.onAcceptEt(); + $httpBackend.flush(); + }); + }); + + describe('province() setter', () => { + it(`should set countryFk property`, () => { + controller.client.countryFk = null; + controller.province = { + id: 1, + name: 'New york', + country: { + id: 2, + name: 'USA' + } + }; + + expect(controller.client.countryFk).toEqual(2); + }); + }); + + describe('town() setter', () => { + it(`should set provinceFk property`, () => { + controller.town = { + provinceFk: 1, + code: 46001, + province: { + id: 1, + name: 'New york', + country: { + id: 2, + name: 'USA' + } + }, + postcodes: [] + }; + + expect(controller.client.provinceFk).toEqual(1); + }); + + it(`should set provinceFk property and fill the postalCode if there's just one`, () => { + controller.town = { + provinceFk: 1, + code: 46001, + province: { + id: 1, + name: 'New york', + country: { + id: 2, + name: 'USA' + } + }, + postcodes: [{code: '46001'}] + }; + + expect(controller.client.provinceFk).toEqual(1); + expect(controller.client.postcode).toEqual('46001'); + }); + }); + + describe('postcode() setter', () => { + it(`should set the town, provinceFk and contryFk properties`, () => { + controller.postcode = { + townFk: 1, + code: 46001, + town: { + id: 1, + name: 'New York', + province: { + id: 1, + name: 'New york', + country: { + id: 2, + name: 'USA' + } + } + } + }; + + expect(controller.client.city).toEqual('New York'); + expect(controller.client.provinceFk).toEqual(1); + expect(controller.client.countryFk).toEqual(2); + }); + }); + }); +}); diff --git a/modules/supplier/front/fiscal-data/locale/es.yml b/modules/supplier/front/fiscal-data/locale/es.yml new file mode 100644 index 000000000..8b98a91af --- /dev/null +++ b/modules/supplier/front/fiscal-data/locale/es.yml @@ -0,0 +1,3 @@ +Sage tax type: Tipo de impuesto Sage +Sage transaction type: Tipo de transacción Sage +Sage withholding: Retención Sage diff --git a/modules/supplier/front/index.js b/modules/supplier/front/index.js index daf19e606..46db8774d 100644 --- a/modules/supplier/front/index.js +++ b/modules/supplier/front/index.js @@ -6,3 +6,5 @@ import './search-panel'; import './summary'; import './card'; import './descriptor'; +import './fiscal-data'; + diff --git a/modules/supplier/front/routes.json b/modules/supplier/front/routes.json index 78ae46985..2e32ad985 100644 --- a/modules/supplier/front/routes.json +++ b/modules/supplier/front/routes.json @@ -9,6 +9,8 @@ {"state": "supplier.index", "icon": "icon-supplier"} ], "card": [ + {"state": "supplier.card.basicData", "icon": "settings"}, + {"state": "supplier.card.fiscalData", "icon": "account_balance"} ] }, "routes": [ @@ -36,6 +38,22 @@ "params": { "supplier": "$ctrl.supplier" } + }, { + "url": "/basic-data", + "state": "supplier.card.basicData", + "component": "vn-supplier-basic-data", + "description": "Basic data", + "params": { + "supplier": "$ctrl.supplier" + } + }, { + "url": "/fiscal-data", + "state": "supplier.card.fiscalData", + "component": "vn-supplier-fiscal-data", + "description": "Fiscal data", + "params": { + "supplier": "$ctrl.supplier" + } } ] } \ No newline at end of file From 9e3186c0df3df0b67ff620c8e1c4cbbaec22b908 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 2 Nov 2020 08:16:42 +0100 Subject: [PATCH 03/12] Updated unit tests --- back/methods/campaign/spec/upcoming.spec.js | 12 +- .../supplier/front/fiscal-data/index.spec.js | 134 ++---------------- 2 files changed, 18 insertions(+), 128 deletions(-) diff --git a/back/methods/campaign/spec/upcoming.spec.js b/back/methods/campaign/spec/upcoming.spec.js index 953683e7a..14bffe3cf 100644 --- a/back/methods/campaign/spec/upcoming.spec.js +++ b/back/methods/campaign/spec/upcoming.spec.js @@ -2,15 +2,11 @@ const app = require('vn-loopback/server/server'); describe('campaign upcoming()', () => { it('should return the upcoming campaign but from the last year', async() => { - let response = await app.models.Campaign.upcoming(); - - const lastYearDate = new Date(); - lastYearDate.setFullYear(lastYearDate.getFullYear() - 1); - const lastYear = lastYearDate.getFullYear(); - + const response = await app.models.Campaign.upcoming(); const campaignDated = response.dated; - const campaignYear = campaignDated.getFullYear(); + const now = new Date(); - expect(campaignYear).toEqual(lastYear); + expect(campaignDated).toEqual(jasmine.any(Date)); + expect(campaignDated).toBeLessThanOrEqual(now); }); }); diff --git a/modules/supplier/front/fiscal-data/index.spec.js b/modules/supplier/front/fiscal-data/index.spec.js index b96de89ac..6fb135c08 100644 --- a/modules/supplier/front/fiscal-data/index.spec.js +++ b/modules/supplier/front/fiscal-data/index.spec.js @@ -3,27 +3,22 @@ import watcher from 'core/mocks/watcher'; describe('Supplier', () => { describe('Component vnSupplierFiscalData', () => { - let $httpBackend; let $scope; let $element; let controller; beforeEach(ngModule('supplier')); - beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { - $httpBackend = _$httpBackend_; + beforeEach(inject(($componentController, $rootScope) => { $scope = $rootScope.$new(); $scope.watcher = watcher; - $scope.watcher.orgData = {id: 101, isEqualizated: false, isTaxDataChecked: false}; + $scope.watcher.orgData = {id: 1}; $element = angular.element(''); - controller = $componentController('vnClientFiscalData', {$element, $scope}); + controller = $componentController('vnSupplierFiscalData', {$element, $scope}); controller.card = {reload: () => {}}; - controller.client = { - id: 101, - email: 'batman@gothamcity.com', - phone: '1111111111', - isEqualizated: false, - isTaxDataChecked: false + controller.supplier = { + id: 1, + name: 'Batman' }; controller._province = {}; @@ -31,110 +26,9 @@ describe('Supplier', () => { controller._postcode = {}; })); - describe('onSubmit()', () => { - it('should call the save() method directly', () => { - jest.spyOn(controller, 'save'); - - controller.onSubmit(); - - expect(controller.save).toHaveBeenCalledWith(); - }); - - it('should call the checkExistingClient() if the isTaxDataChecked property is checked', () => { - jest.spyOn(controller, 'save'); - jest.spyOn(controller, 'checkExistingClient'); - - controller.client.isTaxDataChecked = true; - controller.onSubmit(); - - expect(controller.save).not.toHaveBeenCalledWith(); - expect(controller.checkExistingClient).toHaveBeenCalledWith(); - }); - }); - - describe('checkExistingClient()', () => { - it(`should make a HTTP GET query filtering by email, phone and mobile`, () => { - controller.client.mobile = 222222222; - const filterObj = { - where: { - and: [ - {or: [ - {email: controller.client.email}, - {phone: controller.client.phone}, - {mobile: controller.client.mobile} - ]}, - {id: {neq: controller.client.id}} - ] - } - }; - const filter = encodeURIComponent(JSON.stringify(filterObj)); - $httpBackend.expect('GET', `Clients/findOne?filter=${filter}`).respond(404); - controller.checkExistingClient(); - $httpBackend.flush(); - }); - - it(`should show a save confirmation and then set the despiteOfClient property`, () => { - controller.$.confirmDuplicatedClient = {show: () => {}}; - jest.spyOn(controller.$.confirmDuplicatedClient, 'show'); - - const filterObj = { - where: { - and: [ - {or: [{email: controller.client.email}, {phone: controller.client.phone}]}, - {id: {neq: controller.client.id}} - ] - } - }; - const expectedClient = {id: 102}; - const filter = encodeURIComponent(JSON.stringify(filterObj)); - $httpBackend.expect('GET', `Clients/findOne?filter=${filter}`).respond(expectedClient); - controller.checkExistingClient(); - $httpBackend.flush(); - - expect(controller.$.confirmDuplicatedClient.show).toHaveBeenCalledWith(); - expect(controller.client.despiteOfClient).toEqual(102); - }); - }); - - describe('checkEtChanges()', () => { - it(`should show a propagation confirmation if isEqualizated property is changed and invoice by address is checked`, () => { - controller.$.propagateIsEqualizated = {show: () => {}}; - jest.spyOn(controller.$.propagateIsEqualizated, 'show'); - - const orgData = $scope.watcher.orgData; - orgData.hasToInvoiceByAddress = true; - controller.client.isEqualizated = true; - - controller.checkEtChanges(orgData); - - expect(controller.$.propagateIsEqualizated.show).toHaveBeenCalledWith(); - }); - - it(`should call to the onAcceptEt() method if isEqualizated property is changed and invoice by address isn't checked`, () => { - jest.spyOn(controller, 'onAcceptEt'); - - const orgData = $scope.watcher.orgData; - orgData.hasToInvoiceByAddress = false; - controller.client.isEqualizated = true; - - controller.checkEtChanges(orgData); - - expect(controller.onAcceptEt).toHaveBeenCalledWith(); - }); - }); - - describe('onAcceptEt()', () => { - it('should request to patch the propagation of tax status', () => { - controller.client = {id: 123, isEqualizated: false}; - $httpBackend.expectPATCH(`Clients/${controller.client.id}/addressesPropagateRe`, {isEqualizated: controller.client.isEqualizated}).respond('done'); - controller.onAcceptEt(); - $httpBackend.flush(); - }); - }); - describe('province() setter', () => { it(`should set countryFk property`, () => { - controller.client.countryFk = null; + controller.supplier.countryFk = null; controller.province = { id: 1, name: 'New york', @@ -144,7 +38,7 @@ describe('Supplier', () => { } }; - expect(controller.client.countryFk).toEqual(2); + expect(controller.supplier.countryFk).toEqual(2); }); }); @@ -164,7 +58,7 @@ describe('Supplier', () => { postcodes: [] }; - expect(controller.client.provinceFk).toEqual(1); + expect(controller.supplier.provinceFk).toEqual(1); }); it(`should set provinceFk property and fill the postalCode if there's just one`, () => { @@ -182,8 +76,8 @@ describe('Supplier', () => { postcodes: [{code: '46001'}] }; - expect(controller.client.provinceFk).toEqual(1); - expect(controller.client.postcode).toEqual('46001'); + expect(controller.supplier.provinceFk).toEqual(1); + expect(controller.supplier.postCode).toEqual('46001'); }); }); @@ -206,9 +100,9 @@ describe('Supplier', () => { } }; - expect(controller.client.city).toEqual('New York'); - expect(controller.client.provinceFk).toEqual(1); - expect(controller.client.countryFk).toEqual(2); + expect(controller.supplier.city).toEqual('New York'); + expect(controller.supplier.provinceFk).toEqual(1); + expect(controller.supplier.countryFk).toEqual(2); }); }); }); From 61e63cdd48669f822bbfbac4eebc99296d02adf0 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 2 Nov 2020 08:53:28 +0100 Subject: [PATCH 04/12] Updated unit tests --- .../back/methods/supplier/specs/getSummary.spec.js | 2 +- .../methods/supplier/specs/updateFiscalData.spec.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js index ec2e89c88..30713f517 100644 --- a/modules/supplier/back/methods/supplier/specs/getSummary.spec.js +++ b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js @@ -7,7 +7,7 @@ describe('Supplier getSummary()', () => { expect(supplier.id).toEqual(1); expect(supplier.name).toEqual('Plants SL'); expect(supplier.nif).toEqual('06089160W'); - expect(supplier.account).toEqual('4000000001'); + expect(supplier.account).toEqual('4100000001'); expect(supplier.payDay).toEqual(15); }); diff --git a/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js index eac9fe899..84e702914 100644 --- a/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js +++ b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js @@ -1,4 +1,5 @@ const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); describe('Supplier updateFiscalData', () => { const supplierId = 1; @@ -7,7 +8,7 @@ describe('Supplier updateFiscalData', () => { const defaultData = { name: 'Plants SL', nif: '06089160W', - account: '4000000001', + account: '4100000001', sageTaxTypeFk: 4, sageWithholdingFk: 1, sageTransactionTypeFk: 1, @@ -46,7 +47,14 @@ describe('Supplier updateFiscalData', () => { }); it('should update the supplier fiscal data and return the count if changes made', async() => { - const ctx = {req: {accessToken: {userId: administrativeId}}}; + const activeCtx = { + accessToken: {userId: administrativeId}, + }; + const ctx = {req: activeCtx}; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + ctx.args = { name: 'Weapon Dealer', nif: 'B11111111', From 590de622f58629547aca85a1925628ac490980ee Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 2 Nov 2020 14:30:33 +0100 Subject: [PATCH 05/12] Added model validations --- db/changes/10250-curfew/00-ACL.sql | 3 + db/dump/fixtures.sql | 4 +- loopback/locale/es.json | 4 +- .../util}/specs/validateIban.spec.js | 0 .../util}/specs/validateTin.spec.js | 0 .../util}/validateIban.js | 0 .../util}/validateTin.js | 0 modules/client/back/models/client.js | 13 ++-- modules/client/front/locale/es.yml | 2 +- .../supplier/specs/updateFiscalData.spec.js | 8 +-- modules/supplier/back/models/supplier.js | 59 +++++++++++++++++++ modules/supplier/front/fiscal-data/index.html | 10 +++- modules/supplier/front/index.js | 3 +- modules/supplier/front/routes.json | 3 +- 14 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 db/changes/10250-curfew/00-ACL.sql rename {modules/client/back/validations => loopback/util}/specs/validateIban.spec.js (100%) rename {modules/client/back/validations => loopback/util}/specs/validateTin.spec.js (100%) rename {modules/client/back/validations => loopback/util}/validateIban.js (100%) rename {modules/client/back/validations => loopback/util}/validateTin.js (100%) diff --git a/db/changes/10250-curfew/00-ACL.sql b/db/changes/10250-curfew/00-ACL.sql new file mode 100644 index 000000000..c4987c405 --- /dev/null +++ b/db/changes/10250-curfew/00-ACL.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Supplier', 'updateFiscalData', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Supplier', '*', 'READ', 'ALLOW', 'ROLE', 'employee'); +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('SupplierLog', '*', 'READ', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 907219511..5cfcf92c6 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -1211,8 +1211,8 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`) INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`) VALUES (1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1), - (2, 'Farmer King', 'The farmer', 4000020002, 1, 'B22222222', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 2, 8), - (442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, 'C33333333', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3); + (2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 2, 8), + (442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3); INSERT INTO `cache`.`cache_calc`(`id`, `cache_id`, `cacheName`, `params`, `last_refresh`, `expires`, `created`, `connection_id`) VALUES diff --git a/loopback/locale/es.json b/loopback/locale/es.json index c3fe287eb..580bd374f 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -156,5 +156,7 @@ "shipped": "F. envío", "landed": "F. entrega", "addressFk": "Consignatario", - "companyFk": "Empresa" + "companyFk": "Empresa", + "The social name cannot be empty": "La razón social no puede quedar en blanco", + "The nif cannot be empty": "El NIF no puede quedar en blanco" } \ No newline at end of file diff --git a/modules/client/back/validations/specs/validateIban.spec.js b/loopback/util/specs/validateIban.spec.js similarity index 100% rename from modules/client/back/validations/specs/validateIban.spec.js rename to loopback/util/specs/validateIban.spec.js diff --git a/modules/client/back/validations/specs/validateTin.spec.js b/loopback/util/specs/validateTin.spec.js similarity index 100% rename from modules/client/back/validations/specs/validateTin.spec.js rename to loopback/util/specs/validateTin.spec.js diff --git a/modules/client/back/validations/validateIban.js b/loopback/util/validateIban.js similarity index 100% rename from modules/client/back/validations/validateIban.js rename to loopback/util/validateIban.js diff --git a/modules/client/back/validations/validateTin.js b/loopback/util/validateTin.js similarity index 100% rename from modules/client/back/validations/validateTin.js rename to loopback/util/validateTin.js diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 36bc60dfa..01e5add6d 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -1,7 +1,9 @@ -let request = require('request-promise-native'); -let UserError = require('vn-loopback/util/user-error'); -let getFinalState = require('vn-loopback/util/hook').getFinalState; -let isMultiple = require('vn-loopback/util/hook').isMultiple; +const request = require('request-promise-native'); +const UserError = require('vn-loopback/util/user-error'); +const getFinalState = require('vn-loopback/util/hook').getFinalState; +const isMultiple = require('vn-loopback/util/hook').isMultiple; +const validateTin = require('vn-loopback/util/validateTin'); +const validateIban = require('vn-loopback/util/validateIban'); const LoopBackContext = require('loopback-context'); module.exports = Self => { @@ -63,7 +65,7 @@ module.exports = Self => { Self.validateAsync('iban', ibanNeedsValidation, { message: 'The IBAN does not have the correct format' }); - let validateIban = require('../validations/validateIban'); + async function ibanNeedsValidation(err, done) { let filter = { fields: ['code'], @@ -83,7 +85,6 @@ module.exports = Self => { message: 'Invalid TIN' }); - let validateTin = require('../validations/validateTin'); async function tinIsValid(err, done) { if (!this.isTaxDataChecked) return done(); diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index e332a0229..166bdbe1b 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -7,7 +7,7 @@ Has to invoice: Factura Notify by email: Notificar vía e-mail Country: País Street: Domicilio fiscal -City: Municipio +City: Ciudad Postcode: Código postal Province: Provincia Address: Consignatario diff --git a/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js index 84e702914..0eec54926 100644 --- a/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js +++ b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js @@ -57,7 +57,7 @@ describe('Supplier updateFiscalData', () => { ctx.args = { name: 'Weapon Dealer', - nif: 'B11111111', + nif: 'A68446004', account: '4000000005', sageTaxTypeFk: 5, sageWithholdingFk: 2, @@ -65,13 +65,13 @@ describe('Supplier updateFiscalData', () => { postCode: '46460', city: 'VALENCIA', provinceFk: 2, - countryFk: 2, + countryFk: 1, }; const result = await app.models.Supplier.updateFiscalData(ctx, supplierId); expect(result.name).toEqual('Weapon Dealer'); - expect(result.nif).toEqual('B11111111'); + expect(result.nif).toEqual('A68446004'); expect(result.account).toEqual('4000000005'); expect(result.sageTaxTypeFk).toEqual(5); expect(result.sageWithholdingFk).toEqual(2); @@ -79,7 +79,7 @@ describe('Supplier updateFiscalData', () => { expect(result.postCode).toEqual('46460'); expect(result.city).toEqual('VALENCIA'); expect(result.provinceFk).toEqual(2); - expect(result.countryFk).toEqual(2); + expect(result.countryFk).toEqual(1); // Restores ctx.args = defaultData; diff --git a/modules/supplier/back/models/supplier.js b/modules/supplier/back/models/supplier.js index d042037e7..1e5773a6b 100644 --- a/modules/supplier/back/models/supplier.js +++ b/modules/supplier/back/models/supplier.js @@ -1,5 +1,64 @@ +const UserError = require('vn-loopback/util/user-error'); +const validateTin = require('vn-loopback/util/validateTin'); + module.exports = Self => { require('../methods/supplier/filter')(Self); require('../methods/supplier/getSummary')(Self); require('../methods/supplier/updateFiscalData')(Self); + + Self.validatesPresenceOf('name', { + message: 'The social name cannot be empty' + }); + + Self.validatesUniquenessOf('name', { + message: 'The supplier name must be unique' + }); + + Self.validatesPresenceOf('city', { + message: 'City cannot be empty' + }); + + Self.validatesPresenceOf('nif', { + message: 'The nif cannot be empty' + }); + + Self.validatesUniquenessOf('nif', { + message: 'TIN must be unique' + }); + + Self.validateAsync('nif', tinIsValid, { + message: 'Invalid TIN' + }); + + async function tinIsValid(err, done) { + const filter = { + fields: ['code'], + where: {id: this.countryFk} + }; + const country = await Self.app.models.Country.findOne(filter); + const code = country ? country.code.toLowerCase() : null; + + if (!this.nif || !validateTin(this.nif, code)) + err(); + done(); + } + + function isAlpha(value) { + const regexp = new RegExp(/^[ñça-zA-Z0-9\s]*$/i); + + return regexp.test(value); + } + + Self.observe('before save', async function(ctx) { + let changes = ctx.data || ctx.instance; + let orgData = ctx.currentInstance; + + const socialName = changes.name || orgData.name; + const hasChanges = orgData && changes; + const socialNameChanged = hasChanges + && orgData.socialName != socialName; + + if ((socialNameChanged) && !isAlpha(socialName)) + throw new UserError('The socialName has an invalid format'); + }); }; diff --git a/modules/supplier/front/fiscal-data/index.html b/modules/supplier/front/fiscal-data/index.html index 32fd38ad4..a36a98441 100644 --- a/modules/supplier/front/fiscal-data/index.html +++ b/modules/supplier/front/fiscal-data/index.html @@ -44,13 +44,15 @@ vn-focus label="Social name" ng-model="$ctrl.supplier.name" - rule - info="You can use letters and spaces"> + info="You can use letters and spaces" + required="true" + rule> @@ -120,7 +122,9 @@ url="Towns/location" fields="['id', 'name', 'provinceFk']" show-field="name" - value-field="name"> + value-field="name" + required="true" + rule> {{name}}, {{province.name}} ({{province.country.country}}) diff --git a/modules/supplier/front/index.js b/modules/supplier/front/index.js index a4017bbd9..5f52bc97f 100644 --- a/modules/supplier/front/index.js +++ b/modules/supplier/front/index.js @@ -5,5 +5,6 @@ import './card'; import './descriptor'; import './index/'; import './search-panel'; -import './log'; import './summary'; +import './fiscal-data'; +import './log'; diff --git a/modules/supplier/front/routes.json b/modules/supplier/front/routes.json index d1f0701a1..a5bc48c9a 100644 --- a/modules/supplier/front/routes.json +++ b/modules/supplier/front/routes.json @@ -54,7 +54,8 @@ "description": "Fiscal data", "params": { "supplier": "$ctrl.supplier" - } + }, + "acl": ["administrative"] }, { "url" : "/log", "state": "supplier.card.log", From b3ab1fe0593416d745bfb6a5d4a7fe2ca809cfa0 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Mon, 2 Nov 2020 19:58:07 +0100 Subject: [PATCH 06/12] Sync fixes & improvements --- .../00-role_syncPrivileges.sql | 503 ++++++++++++++++++ db/changes/10240-allSaints/00-sambaConfig.sql | 10 + .../account/back/methods/role-inherit/sync.js | 113 +--- .../back/methods/user-account/sync-all.js | 11 +- .../account/back/methods/user-account/sync.js | 8 +- modules/account/back/models/ldap-config.json | 7 +- modules/account/back/models/samba-config.json | 11 +- modules/account/back/util/sync-connector.js | 5 + modules/account/back/util/sync-engine.js | 38 +- modules/account/back/util/sync-ldap.js | 143 ++++- modules/account/back/util/sync-samba.js | 102 ++-- modules/account/front/ldap/index.html | 22 +- modules/account/front/ldap/index.js | 4 +- modules/account/front/ldap/locale/es.yml | 10 +- modules/account/front/samba/index.html | 33 +- modules/account/front/samba/locale/es.yml | 9 +- 16 files changed, 801 insertions(+), 228 deletions(-) create mode 100644 db/changes/10240-allSaints/00-role_syncPrivileges.sql create mode 100644 db/changes/10240-allSaints/00-sambaConfig.sql diff --git a/db/changes/10240-allSaints/00-role_syncPrivileges.sql b/db/changes/10240-allSaints/00-role_syncPrivileges.sql new file mode 100644 index 000000000..0e3b0d55b --- /dev/null +++ b/db/changes/10240-allSaints/00-role_syncPrivileges.sql @@ -0,0 +1,503 @@ +DROP PROCEDURE IF EXISTS account.role_syncPrivileges; +DELIMITER $$ +CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_syncPrivileges`() +BEGIN +/** + * Synchronizes permissions of MySQL role users based on role hierarchy. + * The computed role users of permission mix will be named according to + * pattern z-[role_name]. + * + * If any@localhost user exists, it will be taken as a template for basic + * attributes. + * + * Warning! This procedure should only be called when MySQL privileges + * are modified. If role hierarchy is modified, you must call the role_sync() + * procedure wich calls this internally. + */ + DECLARE vIsMysql BOOL DEFAULT VERSION() NOT LIKE '%MariaDB%'; + DECLARE vVersion INT DEFAULT SUBSTRING_INDEX(VERSION(), '.', 1); + DECLARE vTplUser VARCHAR(255) DEFAULT 'any'; + DECLARE vTplHost VARCHAR(255) DEFAULT '%'; + DECLARE vRoleHost VARCHAR(255) DEFAULT 'localhost'; + DECLARE vAllHost VARCHAR(255) DEFAULT '%'; + DECLARE vPrefix VARCHAR(2) DEFAULT 'z-'; + DECLARE vPrefixedLike VARCHAR(255); + DECLARE vPassword VARCHAR(255) DEFAULT ''; + + -- Deletes computed role users + + SET vPrefixedLike = CONCAT(vPrefix, '%'); + + IF vIsMysql THEN + DELETE FROM mysql.user + WHERE `User` LIKE vPrefixedLike; + ELSE + DELETE FROM mysql.global_priv + WHERE `User` LIKE vPrefixedLike; + END IF; + + DELETE FROM mysql.db + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.tables_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.columns_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.procs_priv + WHERE `User` LIKE vPrefixedLike; + + DELETE FROM mysql.proxies_priv + WHERE `Proxied_user` LIKE vPrefixedLike; + + -- Temporary tables + + DROP TEMPORARY TABLE IF EXISTS tRole; + CREATE TEMPORARY TABLE tRole + (INDEX (id)) + ENGINE = MEMORY + SELECT + id, + `name` role, + CONCAT(vPrefix, `name`) prefixedRole + FROM role + WHERE hasLogin; + + DROP TEMPORARY TABLE IF EXISTS tRoleInherit; + CREATE TEMPORARY TABLE tRoleInherit + (INDEX (inheritsFrom)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + ri.`name` inheritsFrom + FROM tRole r + JOIN roleRole rr ON rr.role = r.id + JOIN role ri ON ri.id = rr.inheritsFrom; + + -- Recreate role users + + IF vIsMysql THEN + DROP TEMPORARY TABLE IF EXISTS tUser; + CREATE TEMPORARY TABLE tUser + SELECT + r.prefixedRole `User`, + vTplHost `Host`, + IFNULL(t.`authentication_string`, + '') `authentication_string`, + IFNULL(t.`plugin`, + 'mysql_native_password') `plugin`, + IFNULL(IF('' != u.`ssl_type`, + u.`ssl_type`, t.`ssl_type`), + '') `ssl_type`, + IFNULL(IF('' != u.`ssl_cipher`, + u.`ssl_cipher`, t.`ssl_cipher`), + '') `ssl_cipher`, + IFNULL(IF('' != u.`x509_issuer`, + u.`x509_issuer`, t.`x509_issuer`), + '') `x509_issuer`, + IFNULL(IF('' != u.`x509_subject`, + u.`x509_subject`, t.`x509_subject`), + '') `x509_subject`, + IFNULL(IF(0 != u.`max_questions`, + u.`max_questions`, t.`max_questions`), + 0) `max_questions`, + IFNULL(IF(0 != u.`max_updates`, + u.`max_updates`, t.`max_updates`), + 0) `max_updates`, + IFNULL(IF(0 != u.`max_connections`, + u.`max_connections`, t.`max_connections`), + 0) `max_connections`, + IFNULL(IF(0 != u.`max_user_connections`, + u.`max_user_connections`, t.`max_user_connections`), + 0) `max_user_connections` + FROM tRole r + LEFT JOIN mysql.user t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.user u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + + IF vVersion <= 5 THEN + SELECT `Password` INTO vPassword + FROM mysql.user + WHERE `User` = vTplUser + AND `Host` = vRoleHost; + + INSERT INTO mysql.user ( + `User`, + `Host`, + `Password`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + vPassword, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + ELSE + INSERT INTO mysql.user ( + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + ) + SELECT + `User`, + `Host`, + `authentication_string`, + `plugin`, + `ssl_type`, + `ssl_cipher`, + `x509_issuer`, + `x509_subject`, + `max_questions`, + `max_updates`, + `max_connections`, + `max_user_connections` + FROM tUser; + END IF; + + DROP TEMPORARY TABLE IF EXISTS tUser; + ELSE + INSERT INTO mysql.global_priv ( + `User`, + `Host`, + `Priv` + ) + SELECT + r.prefixedRole, + vTplHost, + JSON_MERGE_PATCH( + IFNULL(t.`Priv`, '{}'), + IFNULL(u.`Priv`, '{}'), + JSON_OBJECT( + 'mysql_old_password', JSON_VALUE(t.`Priv`, '$.mysql_old_password'), + 'mysql_native_password', JSON_VALUE(t.`Priv`, '$.mysql_native_password'), + 'authentication_string', JSON_VALUE(t.`Priv`, '$.authentication_string') + ) + ) + FROM tRole r + LEFT JOIN mysql.global_priv t + ON t.`User` = vTplUser + AND t.`Host` = vRoleHost + LEFT JOIN mysql.global_priv u + ON u.`User` = r.role + AND u.`Host` = vRoleHost; + END IF; + + INSERT INTO mysql.proxies_priv ( + `User`, + `Host`, + `Proxied_user`, + `Proxied_host`, + `Grantor` + ) + SELECT + '', + vAllHost, + prefixedRole, + vTplHost, + CONCAT(prefixedRole, '@', vTplHost) + FROM tRole; + + -- Copies global privileges + + DROP TEMPORARY TABLE IF EXISTS tUserPriv; + + IF vIsMysql THEN + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + ENGINE = MEMORY + SELECT + r.prefixedRole, + MAX(u.`Select_priv`) `Select_priv`, + MAX(u.`Insert_priv`) `Insert_priv`, + MAX(u.`Update_priv`) `Update_priv`, + MAX(u.`Delete_priv`) `Delete_priv`, + MAX(u.`Create_priv`) `Create_priv`, + MAX(u.`Drop_priv`) `Drop_priv`, + MAX(u.`Reload_priv`) `Reload_priv`, + MAX(u.`Shutdown_priv`) `Shutdown_priv`, + MAX(u.`Process_priv`) `Process_priv`, + MAX(u.`File_priv`) `File_priv`, + MAX(u.`Grant_priv`) `Grant_priv`, + MAX(u.`References_priv`) `References_priv`, + MAX(u.`Index_priv`) `Index_priv`, + MAX(u.`Alter_priv`) `Alter_priv`, + MAX(u.`Show_db_priv`) `Show_db_priv`, + MAX(u.`Super_priv`) `Super_priv`, + MAX(u.`Create_tmp_table_priv`) `Create_tmp_table_priv`, + MAX(u.`Lock_tables_priv`) `Lock_tables_priv`, + MAX(u.`Execute_priv`) `Execute_priv`, + MAX(u.`Repl_slave_priv`) `Repl_slave_priv`, + MAX(u.`Repl_client_priv`) `Repl_client_priv`, + MAX(u.`Create_view_priv`) `Create_view_priv`, + MAX(u.`Show_view_priv`) `Show_view_priv`, + MAX(u.`Create_routine_priv`) `Create_routine_priv`, + MAX(u.`Alter_routine_priv`) `Alter_routine_priv`, + MAX(u.`Create_user_priv`) `Create_user_priv`, + MAX(u.`Event_priv`) `Event_priv`, + MAX(u.`Trigger_priv`) `Trigger_priv`, + MAX(u.`Create_tablespace_priv`) `Create_tablespace_priv` + FROM tRoleInherit r + JOIN mysql.user u + ON u.`User` = r.inheritsFrom + AND u.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.user u + JOIN tUserPriv t + ON u.`User` = t.prefixedRole + AND u.`Host` = vTplHost + SET + u.`Select_priv` + = t.`Select_priv`, + u.`Insert_priv` + = t.`Insert_priv`, + u.`Update_priv` + = t.`Update_priv`, + u.`Delete_priv` + = t.`Delete_priv`, + u.`Create_priv` + = t.`Create_priv`, + u.`Drop_priv` + = t.`Drop_priv`, + u.`Reload_priv` + = t.`Reload_priv`, + u.`Shutdown_priv` + = t.`Shutdown_priv`, + u.`Process_priv` + = t.`Process_priv`, + u.`File_priv` + = t.`File_priv`, + u.`Grant_priv` + = t.`Grant_priv`, + u.`References_priv` + = t.`References_priv`, + u.`Index_priv` + = t.`Index_priv`, + u.`Alter_priv` + = t.`Alter_priv`, + u.`Show_db_priv` + = t.`Show_db_priv`, + u.`Super_priv` + = t.`Super_priv`, + u.`Create_tmp_table_priv` + = t.`Create_tmp_table_priv`, + u.`Lock_tables_priv` + = t.`Lock_tables_priv`, + u.`Execute_priv` + = t.`Execute_priv`, + u.`Repl_slave_priv` + = t.`Repl_slave_priv`, + u.`Repl_client_priv` + = t.`Repl_client_priv`, + u.`Create_view_priv` + = t.`Create_view_priv`, + u.`Show_view_priv` + = t.`Show_view_priv`, + u.`Create_routine_priv` + = t.`Create_routine_priv`, + u.`Alter_routine_priv` + = t.`Alter_routine_priv`, + u.`Create_user_priv` + = t.`Create_user_priv`, + u.`Event_priv` + = t.`Event_priv`, + u.`Trigger_priv` + = t.`Trigger_priv`, + u.`Create_tablespace_priv` + = t.`Create_tablespace_priv`; + ELSE + CREATE TEMPORARY TABLE tUserPriv + (INDEX (prefixedRole)) + SELECT + r.prefixedRole, + BIT_OR(JSON_VALUE(p.`Priv`, '$.access')) access + FROM tRoleInherit r + JOIN mysql.global_priv p + ON p.`User` = r.inheritsFrom + AND p.`Host`= vRoleHost + GROUP BY r.prefixedRole; + + UPDATE mysql.global_priv p + JOIN tUserPriv t + ON p.`User` = t.prefixedRole + AND p.`Host` = vTplHost + SET + p.`Priv` = JSON_SET(p.`Priv`, '$.access', t.access); + END IF; + + DROP TEMPORARY TABLE tUserPriv; + + -- Copy schema level privileges + + INSERT INTO mysql.db ( + `User`, + `Host`, + `Db`, + `Select_priv`, + `Insert_priv`, + `Update_priv`, + `Delete_priv`, + `Create_priv`, + `Drop_priv`, + `Grant_priv`, + `References_priv`, + `Index_priv`, + `Alter_priv`, + `Create_tmp_table_priv`, + `Lock_tables_priv`, + `Create_view_priv`, + `Show_view_priv`, + `Create_routine_priv`, + `Alter_routine_priv`, + `Execute_priv`, + `Event_priv`, + `Trigger_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + MAX(t.`Select_priv`), + MAX(t.`Insert_priv`), + MAX(t.`Update_priv`), + MAX(t.`Delete_priv`), + MAX(t.`Create_priv`), + MAX(t.`Drop_priv`), + MAX(t.`Grant_priv`), + MAX(t.`References_priv`), + MAX(t.`Index_priv`), + MAX(t.`Alter_priv`), + MAX(t.`Create_tmp_table_priv`), + MAX(t.`Lock_tables_priv`), + MAX(t.`Create_view_priv`), + MAX(t.`Show_view_priv`), + MAX(t.`Create_routine_priv`), + MAX(t.`Alter_routine_priv`), + MAX(t.`Execute_priv`), + MAX(t.`Event_priv`), + MAX(t.`Trigger_priv`) + FROM tRoleInherit r + JOIN mysql.db t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`; + + -- Copy table level privileges + + INSERT INTO mysql.tables_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Grantor`, + `Timestamp`, + `Table_priv`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Grantor`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Table_priv`, '')), ''), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.tables_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`; + + -- Copy column level privileges + + INSERT INTO mysql.columns_priv ( + `User`, + `Host`, + `Db`, + `Table_name`, + `Column_name`, + `Timestamp`, + `Column_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Table_name`, + t.`Column_name`, + MAX(t.`Timestamp`), + IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '') + FROM tRoleInherit r + JOIN mysql.columns_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost + GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`, t.`Column_name`; + + -- Copy routine privileges + + INSERT IGNORE INTO mysql.procs_priv ( + `User`, + `Host`, + `Db`, + `Routine_name`, + `Routine_type`, + `Grantor`, + `Timestamp`, + `Proc_priv` + ) + SELECT + r.prefixedRole, + vTplHost, + t.`Db`, + t.`Routine_name`, + t.`Routine_type`, + t.`Grantor`, + t.`Timestamp`, + t.`Proc_priv` + FROM tRoleInherit r + JOIN mysql.procs_priv t + ON t.`User` = r.inheritsFrom + AND t.`Host`= vRoleHost; + + -- Free memory + + DROP TEMPORARY TABLE + tRole, + tRoleInherit; + + FLUSH PRIVILEGES; +END$$ +DELIMITER ; diff --git a/db/changes/10240-allSaints/00-sambaConfig.sql b/db/changes/10240-allSaints/00-sambaConfig.sql new file mode 100644 index 000000000..e92d62cff --- /dev/null +++ b/db/changes/10240-allSaints/00-sambaConfig.sql @@ -0,0 +1,10 @@ +ALTER TABLE account.sambaConfig ADD adUser VARCHAR(255) DEFAULT NULL NULL COMMENT 'Active directory user'; +ALTER TABLE account.sambaConfig ADD adPassword varchar(255) DEFAULT NULL NULL COMMENT 'Active directory password'; +ALTER TABLE account.sambaConfig ADD userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN for users'; +ALTER TABLE account.sambaConfig DROP COLUMN uidBase; +ALTER TABLE account.sambaConfig CHANGE sshPass sshPassword varchar(255) DEFAULT NULL NULL COMMENT 'The SSH password'; + +ALTER TABLE account.ldapConfig DROP COLUMN `filter`; +ALTER TABLE account.ldapConfig CHANGE baseDn userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN to do the query'; +ALTER TABLE account.ldapConfig CHANGE host server varchar(255) NOT NULL COMMENT 'The hostname of LDAP server'; +ALTER TABLE account.ldapConfig MODIFY COLUMN password varchar(255) NOT NULL COMMENT 'The LDAP password'; diff --git a/modules/account/back/methods/role-inherit/sync.js b/modules/account/back/methods/role-inherit/sync.js index 44a9a7932..b84e4c2fe 100644 --- a/modules/account/back/methods/role-inherit/sync.js +++ b/modules/account/back/methods/role-inherit/sync.js @@ -1,4 +1,5 @@ -const ldap = require('../../util/ldapjs-extra'); + +const SyncEngine = require('../../util/sync-engine'); module.exports = Self => { Self.remoteMethod('sync', { @@ -10,119 +11,17 @@ module.exports = Self => { }); Self.sync = async function() { - let $ = Self.app.models; - - let ldapConfig = await $.LdapConfig.findOne({ - fields: ['host', 'rdn', 'password', 'groupDn'] - }); - let accountConfig = await $.AccountConfig.findOne({ - fields: ['idBase'] - }); - - if (!ldapConfig) return; - - // Connect - - let client = ldap.createClient({ - url: `ldap://${ldapConfig.host}:389` - }); - - let ldapPassword = Buffer - .from(ldapConfig.password, 'base64') - .toString('ascii'); - await client.bind(ldapConfig.rdn, ldapPassword); + let engine = new SyncEngine(); + await engine.init(Self.app.models); let err; try { - // Delete roles - - let opts = { - scope: 'sub', - attributes: ['dn'], - filter: 'objectClass=posixGroup' - }; - let res = await client.search(ldapConfig.groupDn, opts); - - let reqs = []; - await new Promise((resolve, reject) => { - res.on('error', err => { - if (err.name === 'NoSuchObjectError') - err = new Error(`Object '${ldapConfig.groupDn}' does not exist`); - reject(err); - }); - res.on('searchEntry', e => { - reqs.push(client.del(e.object.dn)); - }); - res.on('end', resolve); - }); - await Promise.all(reqs); - - // Recreate roles - - let roles = await $.Role.find({ - fields: ['id', 'name'] - }); - let roleRoles = await $.RoleRole.find({ - fields: ['role', 'inheritsFrom'] - }); - let roleMap = toMap(roleRoles, e => { - return {key: e.inheritsFrom, val: e.role}; - }); - - let accounts = await $.UserAccount.find({ - fields: ['id'], - include: { - relation: 'user', - scope: { - fields: ['name', 'roleFk'], - where: {active: true} - } - } - }); - let accountMap = toMap(accounts, e => { - let user = e.user(); - if (!user) return; - return {key: user.roleFk, val: user.name}; - }); - - reqs = []; - for (let role of roles) { - let newEntry = { - objectClass: ['top', 'posixGroup'], - cn: role.name, - gidNumber: accountConfig.idBase + role.id - }; - - let memberUid = []; - for (subrole of roleMap.get(role.id) || []) - memberUid = memberUid.concat(accountMap.get(subrole) || []); - - if (memberUid.length) { - memberUid.sort((a, b) => a.localeCompare(b)); - newEntry.memberUid = memberUid; - } - - let dn = `cn=${role.name},${ldapConfig.groupDn}`; - reqs.push(client.add(dn, newEntry)); - } - await Promise.all(reqs); + await engine.syncRoles(); } catch (e) { err = e; } - await client.unbind(); + await engine.deinit(); if (err) throw err; }; }; - -function toMap(array, fn) { - let map = new Map(); - for (let item of array) { - let keyVal = fn(item); - if (!keyVal) continue; - let key = keyVal.key; - if (!map.has(key)) map.set(key, []); - map.get(key).push(keyVal.val); - } - return map; -} diff --git a/modules/account/back/methods/user-account/sync-all.js b/modules/account/back/methods/user-account/sync-all.js index 3c9d0c778..737099ce8 100644 --- a/modules/account/back/methods/user-account/sync-all.js +++ b/modules/account/back/methods/user-account/sync-all.js @@ -13,25 +13,24 @@ module.exports = Self => { Self.syncAll = async function() { let $ = Self.app.models; - let se = new SyncEngine(); - await se.init($); + let engine = new SyncEngine(); + await engine.init($); - let usersToSync = await se.getUsers(); + let usersToSync = await engine.getUsers(); usersToSync = Array.from(usersToSync.values()) .sort((a, b) => a.localeCompare(b)); for (let user of usersToSync) { try { console.log(`Synchronizing user '${user}'`); - await se.sync(user); + await engine.sync(user); console.log(` -> '${user}' sinchronized`); } catch (err) { console.error(` -> '${user}' synchronization error:`, err.message); } } - await se.deinit(); - + await engine.deinit(); await $.RoleInherit.sync(); }; }; diff --git a/modules/account/back/methods/user-account/sync.js b/modules/account/back/methods/user-account/sync.js index eff316c92..f16b2c052 100644 --- a/modules/account/back/methods/user-account/sync.js +++ b/modules/account/back/methods/user-account/sync.js @@ -34,17 +34,17 @@ module.exports = Self => { if (user && isSync) return; let err; - let se = new SyncEngine(); - await se.init($); + let engine = new SyncEngine(); + await engine.init($); try { - await se.sync(userName, password, true); + await engine.sync(userName, password, true); await $.UserSync.destroyById(userName); } catch (e) { err = e; } - await se.deinit(); + await engine.deinit(); if (err) throw err; }; }; diff --git a/modules/account/back/models/ldap-config.json b/modules/account/back/models/ldap-config.json index e3061d651..f7d3ab08b 100644 --- a/modules/account/back/models/ldap-config.json +++ b/modules/account/back/models/ldap-config.json @@ -11,7 +11,7 @@ "type": "number", "id": true }, - "host": { + "server": { "type": "string", "required": true }, @@ -23,10 +23,7 @@ "type": "string", "required": true }, - "baseDn": { - "type": "string" - }, - "filter": { + "userDn": { "type": "string" }, "groupDn": { diff --git a/modules/account/back/models/samba-config.json b/modules/account/back/models/samba-config.json index ffbcce4eb..d729ca111 100644 --- a/modules/account/back/models/samba-config.json +++ b/modules/account/back/models/samba-config.json @@ -18,7 +18,16 @@ "sshUser": { "type": "string" }, - "sshPass": { + "sshPassword": { + "type": "string" + }, + "adUser": { + "type": "string" + }, + "adPassword": { + "type": "string" + }, + "userDn": { "type": "string" } } diff --git a/modules/account/back/util/sync-connector.js b/modules/account/back/util/sync-connector.js index 4b8c9ed6a..db7d230be 100644 --- a/modules/account/back/util/sync-connector.js +++ b/modules/account/back/util/sync-connector.js @@ -37,6 +37,11 @@ class SyncConnector { */ async syncGroups(user, userName) {} + /** + * Synchronizes roles. + */ + async syncRoles() {} + /** * Deinitalizes the connector. */ diff --git a/modules/account/back/util/sync-engine.js b/modules/account/back/util/sync-engine.js index 5ca9cac65..6fd256c98 100644 --- a/modules/account/back/util/sync-engine.js +++ b/modules/account/back/util/sync-engine.js @@ -19,8 +19,10 @@ module.exports = class SyncEngine { for (let ConnectorClass of SyncConnector.connectors) { let connector = new ConnectorClass(); Object.assign(connector, { - se: this, - $ + engine: this, + $, + accountConfig, + mailConfig }); if (!await connector.init()) continue; connectors.push(connector); @@ -80,27 +82,20 @@ module.exports = class SyncEngine { } }); - let extraParams; - let hasAccount = false; - - if (user) { - hasAccount = user.active - && await $.UserAccount.exists(user.id); - - extraParams = { - corporateMail: `${userName}@${mailConfig.domain}`, - uidNumber: accountConfig.idBase + user.id - }; - } - let info = { user, - extraParams, - hasAccount, - accountConfig, - mailConfig + hasAccount: false }; + if (user) { + let exists = await $.UserAccount.exists(user.id); + Object.assign(info, { + hasAccount: user.active && exists, + corporateMail: `${userName}@${mailConfig.domain}`, + uidNumber: accountConfig.idBase + user.id + }); + } + let errs = []; for (let connector of this.connectors) { @@ -116,6 +111,11 @@ module.exports = class SyncEngine { if (errs.length) throw errs[0]; } + async syncRoles() { + for (let connector of this.connectors) + await connector.syncRoles(); + } + async getUsers() { let usersToSync = new Set(); diff --git a/modules/account/back/util/sync-ldap.js b/modules/account/back/util/sync-ldap.js index d72520ed6..3f98633d9 100644 --- a/modules/account/back/util/sync-ldap.js +++ b/modules/account/back/util/sync-ldap.js @@ -7,18 +7,20 @@ const crypto = require('crypto'); class SyncLdap extends SyncConnector { async init() { let ldapConfig = await this.$.LdapConfig.findOne({ - fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn'] + fields: [ + 'server', + 'rdn', + 'password', + 'userDn', + 'groupDn' + ] }); if (!ldapConfig) return false; let client = ldap.createClient({ - url: `ldap://${ldapConfig.host}:389` + url: ldapConfig.server }); - - let ldapPassword = Buffer - .from(ldapConfig.password, 'base64') - .toString('ascii'); - await client.bind(ldapConfig.rdn, ldapPassword); + await client.bind(ldapConfig.rdn, ldapConfig.password); Object.assign(this, { ldapConfig, @@ -36,16 +38,12 @@ class SyncLdap extends SyncConnector { let { ldapConfig, client, + accountConfig } = this; - let { - user, - hasAccount, - extraParams, - accountConfig - } = info; + let {user} = info; - let res = await client.search(ldapConfig.baseDn, { + let res = await client.search(ldapConfig.userDn, { scope: 'sub', attributes: ['userPassword', 'sambaNTPassword'], filter: `&(uid=${userName})` @@ -59,13 +57,13 @@ class SyncLdap extends SyncConnector { }); try { - let dn = `uid=${userName},${ldapConfig.baseDn}`; + let dn = `uid=${userName},${ldapConfig.userDn}`; await client.del(dn); } catch (e) { if (e.name !== 'NoSuchObjectError') throw e; } - if (!hasAccount) { + if (!info.hasAccount) { if (oldUser) console.log(` -> '${userName}' removed from LDAP`); return; @@ -77,7 +75,7 @@ class SyncLdap extends SyncConnector { ? nameArgs.splice(1).join(' ') : '-'; - let dn = `uid=${userName},${ldapConfig.baseDn}`; + let dn = `uid=${userName},${ldapConfig.userDn}`; let newEntry = { uid: userName, objectClass: [ @@ -89,11 +87,11 @@ class SyncLdap extends SyncConnector { displayName: nickname, givenName: nameArgs[0], sn, - mail: extraParams.corporateMail, + mail: info.corporateMail, preferredLanguage: user.lang, homeDirectory: `${accountConfig.homedir}/${userName}`, loginShell: accountConfig.shell, - uidNumber: extraParams.uidNumber, + uidNumber: info.uidNumber, gidNumber: accountConfig.idBase + user.roleFk, sambaSID: '-' }; @@ -137,11 +135,6 @@ class SyncLdap extends SyncConnector { client } = this; - let { - user, - hasAccount - } = info; - let res = await client.search(ldapConfig.groupDn, { scope: 'sub', attributes: ['dn'], @@ -165,10 +158,10 @@ class SyncLdap extends SyncConnector { } await Promise.all(reqs); - if (!hasAccount) return; + if (!info.hasAccount) return; reqs = []; - for (let role of user.roles()) { + for (let role of info.user.roles()) { let change = new ldap.Change({ operation: 'add', modification: {memberUid: userName} @@ -186,7 +179,7 @@ class SyncLdap extends SyncConnector { client } = this; - let res = await client.search(ldapConfig.baseDn, { + let res = await client.search(ldapConfig.userDn, { scope: 'sub', attributes: ['uid'], filter: `uid=*` @@ -198,7 +191,103 @@ class SyncLdap extends SyncConnector { res.on('end', resolve); }); } + + async syncRoles() { + let { + $, + ldapConfig, + client, + accountConfig + } = this; + + // Delete roles + + let opts = { + scope: 'sub', + attributes: ['dn'], + filter: 'objectClass=posixGroup' + }; + let res = await client.search(ldapConfig.groupDn, opts); + + let reqs = []; + await new Promise((resolve, reject) => { + res.on('error', err => { + if (err.name === 'NoSuchObjectError') + err = new Error(`Object '${ldapConfig.groupDn}' does not exist`); + reject(err); + }); + res.on('searchEntry', e => { + reqs.push(client.del(e.object.dn)); + }); + res.on('end', resolve); + }); + await Promise.all(reqs); + + // Recreate roles + + let roles = await $.Role.find({ + fields: ['id', 'name'] + }); + let roleRoles = await $.RoleRole.find({ + fields: ['role', 'inheritsFrom'] + }); + let roleMap = toMap(roleRoles, e => { + return {key: e.inheritsFrom, val: e.role}; + }); + + let accounts = await $.UserAccount.find({ + fields: ['id'], + include: { + relation: 'user', + scope: { + fields: ['name', 'roleFk'], + where: {active: true} + } + } + }); + let accountMap = toMap(accounts, e => { + let user = e.user(); + if (!user) return; + return {key: user.roleFk, val: user.name}; + }); + + console.log; + + reqs = []; + for (let role of roles) { + let newEntry = { + objectClass: ['top', 'posixGroup'], + cn: role.name, + gidNumber: accountConfig.idBase + role.id + }; + + let memberUid = []; + for (let subrole of roleMap.get(role.id) || []) + memberUid = memberUid.concat(accountMap.get(subrole) || []); + + if (memberUid.length) { + memberUid.sort((a, b) => a.localeCompare(b)); + newEntry.memberUid = memberUid; + } + + let dn = `cn=${role.name},${ldapConfig.groupDn}`; + reqs.push(client.add(dn, newEntry)); + } + await Promise.all(reqs); + } } SyncConnector.connectors.push(SyncLdap); module.exports = SyncLdap; + +function toMap(array, fn) { + let map = new Map(); + for (let item of array) { + let keyVal = fn(item); + if (!keyVal) continue; + let key = keyVal.key; + if (!map.has(key)) map.set(key, []); + map.get(key).push(keyVal.val); + } + return map; +} diff --git a/modules/account/back/util/sync-samba.js b/modules/account/back/util/sync-samba.js index c6ef556e5..6e5ef9d5a 100644 --- a/modules/account/back/util/sync-samba.js +++ b/modules/account/back/util/sync-samba.js @@ -1,60 +1,71 @@ const SyncConnector = require('./sync-connector'); const ssh = require('node-ssh'); +const ldap = require('./ldapjs-extra'); class SyncSamba extends SyncConnector { async init() { let sambaConfig = await this.$.SambaConfig.findOne({ - fields: ['host', 'sshUser', 'sshPass'] + fields: [ + 'host', + 'sshUser', + 'sshPassword', + 'adUser', + 'adPassword', + 'userDn' + ] }); if (!sambaConfig) return false; - let sshPassword = Buffer - .from(sambaConfig.sshPass, 'base64') - .toString('ascii'); - let client = new ssh.NodeSSH(); await client.connect({ host: sambaConfig.host, username: sambaConfig.sshUser, - password: sshPassword + password: sambaConfig.sshPassword + }); + + let adClient = ldap.createClient({ + url: `ldaps://${sambaConfig.host}:636`, + tlsOptions: {rejectUnauthorized: false} }); Object.assign(this, { sambaConfig, - client + client, + adClient }); return true; } async deinit() { - if (this.client) - await this.client.dispose(); + if (!this.client) return; + await this.client.dispose(); + await this.adClient.unbind(); } async sync(info, userName, password) { - let { - client - } = this; + let {client} = this; - let { - hasAccount, - extraParams - } = info; - - if (hasAccount) { + if (info.hasAccount) { try { await client.exec('samba-tool user create', [ userName, - '--uid-number', `${extraParams.uidNumber}`, - '--mail-address', extraParams.corporateMail, + '--uid-number', `${info.uidNumber}`, + '--mail-address', info.corporateMail, '--random-password' ]); + await client.exec('samba-tool user setexpiry', [ + userName, + '--noexpiry' + ]); + await client.exec('mkhomedir_helper', [ + userName, + '0027' + ]); } catch (e) {} - await client.exec('samba-tool user setexpiry', [ - userName, - '--noexpiry' + await client.exec('samba-tool user enable', [ + userName ]); if (password) { @@ -62,15 +73,7 @@ class SyncSamba extends SyncConnector { userName, '--newpassword', password ]); - await client.exec('samba-tool user enable', [ - userName - ]); } - - await client.exec('mkhomedir_helper', [ - userName, - '0027' - ]); } else { try { await client.exec('samba-tool user disable', [ @@ -81,11 +84,40 @@ class SyncSamba extends SyncConnector { } } + /** + * Gets enabled users from Samba. + * + * Summary of userAccountControl flags: + * https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties + * + * @param {Set} usersToSync + */ async getUsers(usersToSync) { - let {client} = this; - let res = await client.execCommand('samba-tool user list'); - let users = res.stdout.split('\n'); - for (let user of users) usersToSync.add(user.trim()); + let { + sambaConfig, + adClient + } = this; + + await adClient.bind(sambaConfig.adUser, sambaConfig.adPassword); + + let opts = { + scope: 'sub', + attributes: ['sAMAccountName'], + filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' + }; + let res = await adClient.search(sambaConfig.userDn, opts); + + await new Promise((resolve, reject) => { + res.on('error', err => { + if (err.name === 'NoSuchObjectError') + err = new Error(`Object '${sambaConfig.userDn}' does not exist`); + reject(err); + }); + res.on('searchEntry', e => { + usersToSync.add(e.object.sAMAccountName); + }); + res.on('end', resolve); + }); } } diff --git a/modules/account/front/ldap/index.html b/modules/account/front/ldap/index.html index 5c9ec7625..2ba1ceda6 100644 --- a/modules/account/front/ldap/index.html +++ b/modules/account/front/ldap/index.html @@ -11,9 +11,17 @@ class="vn-w-md"> + + + + @@ -25,18 +33,12 @@ - - this.vnApp.showSuccess(this.$t('LDAP users synchronized'))); + .then(() => this.vnApp.showSuccess(this.$t('Users synchronized!'))); } onUserSync() { @@ -15,7 +15,7 @@ export default class Controller extends Section { let params = {password: this.syncPassword}; return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params) - .then(() => this.vnApp.showSuccess(this.$t('User synchronized'))); + .then(() => this.vnApp.showSuccess(this.$t('User synchronized!'))); } onSyncClose() { diff --git a/modules/account/front/ldap/locale/es.yml b/modules/account/front/ldap/locale/es.yml index e7481699f..56fe623e8 100644 --- a/modules/account/front/ldap/locale/es.yml +++ b/modules/account/front/ldap/locale/es.yml @@ -1,7 +1,7 @@ -Host: Host +Enable synchronization: Habilitar sincronización +Server: Servidor RDN: RDN -Base DN: DN base -Password should be base64 encoded: La contraseña debe estar codificada en base64 +User DN: DN usuarios Filter: Filtro Group DN: DN grupos Synchronize now: Sincronizar ahora @@ -9,8 +9,8 @@ Synchronize user: Sincronizar usuario If password is not specified, just user attributes are synchronized: >- Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario Synchronizing in the background: Sincronizando en segundo plano -LDAP users synchronized: Usuarios LDAP sincronizados +Users synchronized!: ¡Usuarios sincronizados! Username: Nombre de usuario Synchronize: Sincronizar Please enter the username: Por favor introduce el nombre de usuario -User synchronized: Usuario sincronizado +User synchronized!: ¡Usuario sincronizado! diff --git a/modules/account/front/samba/index.html b/modules/account/front/samba/index.html index 57461cb14..9b66cdc2a 100644 --- a/modules/account/front/samba/index.html +++ b/modules/account/front/samba/index.html @@ -11,24 +11,47 @@ class="vn-w-md"> + + + + + + + + + + diff --git a/modules/account/front/samba/locale/es.yml b/modules/account/front/samba/locale/es.yml index 7cfc4c744..a036bb3cc 100644 --- a/modules/account/front/samba/locale/es.yml +++ b/modules/account/front/samba/locale/es.yml @@ -1,2 +1,7 @@ -SSH host: Host SSH -Password should be base64 encoded: La contraseña debe estar codificada en base64 +Enable synchronization: Habilitar sincronización +Host: Host +SSH user: Usuario SSH +SSH password: Contraseña SSH +AD user: Usuario AD +AD password: Contraseña AD +User DN: DN usuarios From fd054550276c2c81962e819d37b1e2121a8a88fb Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 4 Nov 2020 11:37:54 +0100 Subject: [PATCH 07/12] Postcode auto-fill fix --- modules/client/front/address/edit/index.js | 2 +- modules/client/front/fiscal-data/index.js | 2 +- modules/supplier/back/models/supplier.js | 21 +++++++++++++++++++ modules/supplier/front/fiscal-data/index.html | 2 +- modules/supplier/front/fiscal-data/index.js | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/modules/client/front/address/edit/index.js b/modules/client/front/address/edit/index.js index d588812fa..30201b880 100644 --- a/modules/client/front/address/edit/index.js +++ b/modules/client/front/address/edit/index.js @@ -52,7 +52,7 @@ export default class Controller extends Section { if (!this.address.provinceFk) this.address.provinceFk = province.id; - if (postcodes.length === 1) + if (!this.address.postalCode && postcodes.length === 1) this.address.postalCode = postcodes[0].code; } diff --git a/modules/client/front/fiscal-data/index.js b/modules/client/front/fiscal-data/index.js index 6aed6e304..65129d3f8 100644 --- a/modules/client/front/fiscal-data/index.js +++ b/modules/client/front/fiscal-data/index.js @@ -128,7 +128,7 @@ export default class Controller extends Section { if (!this.client.countryFk) this.client.countryFk = country.id; - if (postcodes.length === 1) + if (!this.client.postcode && postcodes.length === 1) this.client.postcode = postcodes[0].code; } diff --git a/modules/supplier/back/models/supplier.js b/modules/supplier/back/models/supplier.js index 1e5773a6b..37c94c266 100644 --- a/modules/supplier/back/models/supplier.js +++ b/modules/supplier/back/models/supplier.js @@ -30,6 +30,27 @@ module.exports = Self => { message: 'Invalid TIN' }); + Self.validatesLengthOf('postCode', { + allowNull: true, + allowBlank: true, + min: 3, max: 10 + }); + + Self.validateAsync('postCode', hasValidPostcode, { + message: `The postcode doesn't exist. Please enter a correct one` + }); + + async function hasValidPostcode(err, done) { + if (!this.postcode) + return done(); + + const models = Self.app.models; + const postcode = await models.Postcode.findById(this.postcode); + + if (!postcode) err(); + done(); + } + async function tinIsValid(err, done) { const filter = { fields: ['code'], diff --git a/modules/supplier/front/fiscal-data/index.html b/modules/supplier/front/fiscal-data/index.html index a36a98441..6dda60941 100644 --- a/modules/supplier/front/fiscal-data/index.html +++ b/modules/supplier/front/fiscal-data/index.html @@ -59,7 +59,7 @@ diff --git a/modules/supplier/front/fiscal-data/index.js b/modules/supplier/front/fiscal-data/index.js index 56588247b..f2929c91f 100644 --- a/modules/supplier/front/fiscal-data/index.js +++ b/modules/supplier/front/fiscal-data/index.js @@ -38,7 +38,7 @@ export default class Controller extends Section { if (!this.supplier.countryFk) this.supplier.countryFk = country.id; - if (postcodes.length === 1) + if (!this.supplier.postCode && postcodes.length === 1) this.supplier.postCode = postcodes[0].code; } From 2b6048ca4d015d5b8dc095e2d16d24c42f1f778f Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 4 Nov 2020 12:45:02 +0100 Subject: [PATCH 08/12] Removed comment block --- modules/supplier/front/fiscal-data/index.html | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/modules/supplier/front/fiscal-data/index.html b/modules/supplier/front/fiscal-data/index.html index 6dda60941..1ea3695d6 100644 --- a/modules/supplier/front/fiscal-data/index.html +++ b/modules/supplier/front/fiscal-data/index.html @@ -152,22 +152,6 @@ rule> - From 0570d83a6dacc9bb9f1fb8cfa64027401ad537d6 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 4 Nov 2020 13:38:09 +0100 Subject: [PATCH 09/12] Requested changes --- loopback/locale/es.json | 2 +- .../back/methods/client/specs/updateFiscalData.spec.js | 9 +++++---- modules/client/front/fiscal-data/index.html | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 7cd362ad8..958c06b6d 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -157,5 +157,5 @@ "landed": "F. entrega", "addressFk": "Consignatario", "companyFk": "Empresa", - "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data" + "You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados" } \ No newline at end of file diff --git a/modules/client/back/methods/client/specs/updateFiscalData.spec.js b/modules/client/back/methods/client/specs/updateFiscalData.spec.js index 9c73e29ec..0b4425db6 100644 --- a/modules/client/back/methods/client/specs/updateFiscalData.spec.js +++ b/modules/client/back/methods/client/specs/updateFiscalData.spec.js @@ -2,10 +2,11 @@ const app = require('vn-loopback/server/server'); describe('Client updateFiscalData', () => { const clientId = 101; + const employeeId = 1; const salesAssistantId = 21; const administrativeId = 5; afterAll(async done => { - const ctx = {req: {accessToken: {userId: 5}}}; + const ctx = {req: {accessToken: {userId: administrativeId}}}; ctx.args = {postcode: 46460}; await app.models.Client.updateFiscalData(ctx, clientId); @@ -13,8 +14,8 @@ describe('Client updateFiscalData', () => { done(); }); - it('should return an error if the user is not administrative and the isTaxDataChecked value is true', async() => { - const ctx = {req: {accessToken: {userId: 1}}}; + it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => { + const ctx = {req: {accessToken: {userId: employeeId}}}; ctx.args = {}; let error; @@ -26,7 +27,7 @@ describe('Client updateFiscalData', () => { expect(error.message).toEqual(`You can't make changes on a client with verified data`); }); - it('should return an error if the user is not d and the isTaxDataChecked value is true', async() => { + it('should return an error if the salesAssistant did not fill the sage data before checking verified data', async() => { const client = await app.models.Client.findById(clientId); await client.updateAttribute('isTaxDataChecked', false); diff --git a/modules/client/front/fiscal-data/index.html b/modules/client/front/fiscal-data/index.html index cc9e53097..0523f9fb6 100644 --- a/modules/client/front/fiscal-data/index.html +++ b/modules/client/front/fiscal-data/index.html @@ -30,7 +30,7 @@ data="sageTransactionTypes" order="transaction"> -
+ Date: Wed, 4 Nov 2020 14:38:15 +0100 Subject: [PATCH 10/12] Test fix --- back/methods/campaign/spec/upcoming.spec.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/back/methods/campaign/spec/upcoming.spec.js b/back/methods/campaign/spec/upcoming.spec.js index 953683e7a..14bffe3cf 100644 --- a/back/methods/campaign/spec/upcoming.spec.js +++ b/back/methods/campaign/spec/upcoming.spec.js @@ -2,15 +2,11 @@ const app = require('vn-loopback/server/server'); describe('campaign upcoming()', () => { it('should return the upcoming campaign but from the last year', async() => { - let response = await app.models.Campaign.upcoming(); - - const lastYearDate = new Date(); - lastYearDate.setFullYear(lastYearDate.getFullYear() - 1); - const lastYear = lastYearDate.getFullYear(); - + const response = await app.models.Campaign.upcoming(); const campaignDated = response.dated; - const campaignYear = campaignDated.getFullYear(); + const now = new Date(); - expect(campaignYear).toEqual(lastYear); + expect(campaignDated).toEqual(jasmine.any(Date)); + expect(campaignDated).toBeLessThanOrEqual(now); }); }); From 074e10d4cad74c3710e2d6d53ed338f728b7ae97 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Wed, 4 Nov 2020 15:20:07 +0100 Subject: [PATCH 11/12] translations file was corrupted on merge fix --- loopback/locale/es.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 958c06b6d..5a4752324 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -157,5 +157,7 @@ "landed": "F. entrega", "addressFk": "Consignatario", "companyFk": "Empresa", + "The social name cannot be empty": "La razón social no puede quedar en blanco", + "The nif cannot be empty": "El NIF no puede quedar en blanco", "You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados" } \ No newline at end of file From 840da5e6bc506a72400ca473f7d9b6dd6aee5413 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Wed, 4 Nov 2020 18:14:08 +0100 Subject: [PATCH 12/12] e2e path for supplier.fiscalData --- e2e/helpers/selectors.js | 15 +++ .../01_summary_and_descriptor.spec.js | 2 +- e2e/paths/13-supplier/03_fiscal_data.spec.js | 108 ++++++++++++++++++ loopback/locale/en.json | 4 +- 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 e2e/paths/13-supplier/03_fiscal_data.spec.js diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 52e359687..02c749b3c 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -923,5 +923,20 @@ export default { thirdContactNotes: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.observation"]', saveButton: 'vn-supplier-contact button[type="submit"]', thirdContactDeleteButton: 'vn-supplier-contact div:nth-child(3) vn-icon-button[icon="delete"]' + }, + supplierBasicData: { + + }, + supplierFiscalData: { + socialName: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.name"]', + taxNumber: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.nif"]', + account: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.account"]', + sageTaxType: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageTaxTypeFk"]', + sageWihholding: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageWithholdingFk"]', + postCode: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.postCode"]', + city: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.city"]', + province: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.provinceFk"]', + country: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.countryFk"]', + saveButton: 'vn-supplier-fiscal-data button[type="submit"]', } }; diff --git a/e2e/paths/13-supplier/01_summary_and_descriptor.spec.js b/e2e/paths/13-supplier/01_summary_and_descriptor.spec.js index 21609fced..591a6116a 100644 --- a/e2e/paths/13-supplier/01_summary_and_descriptor.spec.js +++ b/e2e/paths/13-supplier/01_summary_and_descriptor.spec.js @@ -79,6 +79,6 @@ describe('Supplier summary & descriptor path', () => { }); it(`should check the client button isn't present since this supplier should not be a client`, async() => { - await page.waitForSelector(selectors.supplierDescriptor.clientButton, {hidden: true}); + await page.waitForSelector(selectors.supplierDescriptor.clientButton, {visible: false}); }); }); diff --git a/e2e/paths/13-supplier/03_fiscal_data.spec.js b/e2e/paths/13-supplier/03_fiscal_data.spec.js new file mode 100644 index 000000000..2d1e4fbed --- /dev/null +++ b/e2e/paths/13-supplier/03_fiscal_data.spec.js @@ -0,0 +1,108 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Supplier fiscal data path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('administrative', 'supplier'); + await page.accessToSearchResult('2'); + await page.accessToSection('supplier.card.fiscalData'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should attempt to edit the fiscal data but fail as the tax number is invalid', async() => { + await page.clearInput(selectors.supplierFiscalData.city); + await page.clearInput(selectors.supplierFiscalData.province); + await page.clearInput(selectors.supplierFiscalData.country); + await page.clearInput(selectors.supplierFiscalData.postCode); + await page.write(selectors.supplierFiscalData.city, 'Valencia'); + await page.clearInput(selectors.supplierFiscalData.socialName); + await page.write(selectors.supplierFiscalData.socialName, 'Farmer King SL'); + await page.clearInput(selectors.supplierFiscalData.taxNumber); + await page.write(selectors.supplierFiscalData.taxNumber, 'invalid tax number'); + await page.clearInput(selectors.supplierFiscalData.account); + await page.write(selectors.supplierFiscalData.account, 'edited account number'); + await page.autocompleteSearch(selectors.supplierFiscalData.sageWihholding, 'retencion estimacion objetiva'); + await page.autocompleteSearch(selectors.supplierFiscalData.sageTaxType, 'operaciones no sujetas'); + + await page.waitToClick(selectors.supplierFiscalData.saveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toBe('Invalid Tax number'); + }); + + it('should save the changes as the tax number is valid this time', async() => { + await page.clearInput(selectors.supplierFiscalData.taxNumber); + await page.write(selectors.supplierFiscalData.taxNumber, '12345678Z'); + + await page.waitToClick(selectors.supplierFiscalData.saveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toBe('Data saved!'); + }); + + it('should reload the section', async() => { + await page.reloadSection('supplier.card.fiscalData'); + }); + + it('should check the socialName was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.socialName, 'value'); + + expect(result).toEqual('Farmer King SL'); + }); + + it('should check the taxNumber was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.taxNumber, 'value'); + + expect(result).toEqual('12345678Z'); + }); + + it('should check the account was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.account, 'value'); + + expect(result).toEqual('edited account number'); + }); + + it('should check the sageWihholding was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageWihholding, 'value'); + + expect(result).toEqual('RETENCION ESTIMACION OBJETIVA'); + }); + + it('should check the sageTaxType was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageTaxType, 'value'); + + expect(result).toEqual('Operaciones no sujetas'); + }); + + it('should check the postCode was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.postCode, 'value'); + + expect(result).toEqual('46000'); + }); + + it('should check the city was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.city, 'value'); + + expect(result).toEqual('Valencia'); + }); + + it('should check the province was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.province, 'value'); + + expect(result).toEqual('Province one (España)'); + }); + + it('should check the country was edited', async() => { + const result = await page.waitToGetProperty(selectors.supplierFiscalData.country, 'value'); + + expect(result).toEqual('España'); + }); +}); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 172da6faf..0081af429 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -82,5 +82,7 @@ "landed": "Landed", "addressFk": "Address", "companyFk": "Company", - "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data" + "You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data", + "The social name cannot be empty": "The social name cannot be empty", + "The nif cannot be empty": "The nif cannot be empty" } \ No newline at end of file