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/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/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 31d9be163..dbf75d666 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -1211,11 +1211,11 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`) (104, 500), (105, 5000); -INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `transactionTypeSageFk`) +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, NULL, NULL), - (2, 'Farmer King', 'The farmer', 4000020002, 1, 'B22222222', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 8), - (442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, 'C33333333', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, NULL, NULL); + (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, '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 `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`) VALUES 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 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 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 6723e865a..b894815b8 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/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/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/getSummary.spec.js b/modules/supplier/back/methods/supplier/specs/getSummary.spec.js index 85d16bbda..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(4100000001); + 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 new file mode 100644 index 000000000..0eec54926 --- /dev/null +++ b/modules/supplier/back/methods/supplier/specs/updateFiscalData.spec.js @@ -0,0 +1,88 @@ +const app = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('Supplier updateFiscalData', () => { + const supplierId = 1; + const administrativeId = 5; + const employeeId = 1; + const defaultData = { + name: 'Plants SL', + nif: '06089160W', + account: '4100000001', + 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 activeCtx = { + accessToken: {userId: administrativeId}, + }; + const ctx = {req: activeCtx}; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + + ctx.args = { + name: 'Weapon Dealer', + nif: 'A68446004', + account: '4000000005', + sageTaxTypeFk: 5, + sageWithholdingFk: 2, + sageTransactionTypeFk: 2, + postCode: '46460', + city: 'VALENCIA', + provinceFk: 2, + countryFk: 1, + }; + + const result = await app.models.Supplier.updateFiscalData(ctx, supplierId); + + expect(result.name).toEqual('Weapon Dealer'); + expect(result.nif).toEqual('A68446004'); + 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(1); + + // 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..37c94c266 100644 --- a/modules/supplier/back/models/supplier.js +++ b/modules/supplier/back/models/supplier.js @@ -1,4 +1,85 @@ +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' + }); + + 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'], + 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/back/models/supplier.json b/modules/supplier/back/models/supplier.json index 01cc5b51c..596aad745 100644 --- a/modules/supplier/back/models/supplier.json +++ b/modules/supplier/back/models/supplier.json @@ -19,7 +19,7 @@ "type": "String" }, "account": { - "type": "Number" + "type": "String" }, "countryFk": { "type": "Number" @@ -64,7 +64,7 @@ "type": "Number" }, "postCode": { - "type": "Number" + "type": "String" }, "payMethodFk": { "type": "Number" @@ -77,7 +77,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..1ea3695d6 --- /dev/null +++ b/modules/supplier/front/fiscal-data/index.html @@ -0,0 +1,165 @@ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + {{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..f2929c91f --- /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 (!this.supplier.postCode && 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..6fb135c08 --- /dev/null +++ b/modules/supplier/front/fiscal-data/index.spec.js @@ -0,0 +1,109 @@ +import './index'; +import watcher from 'core/mocks/watcher'; + +describe('Supplier', () => { + describe('Component vnSupplierFiscalData', () => { + let $scope; + let $element; + let controller; + + beforeEach(ngModule('supplier')); + + beforeEach(inject(($componentController, $rootScope) => { + $scope = $rootScope.$new(); + $scope.watcher = watcher; + $scope.watcher.orgData = {id: 1}; + $element = angular.element(''); + controller = $componentController('vnSupplierFiscalData', {$element, $scope}); + controller.card = {reload: () => {}}; + controller.supplier = { + id: 1, + name: 'Batman' + }; + + controller._province = {}; + controller._town = {}; + controller._postcode = {}; + })); + + describe('province() setter', () => { + it(`should set countryFk property`, () => { + controller.supplier.countryFk = null; + controller.province = { + id: 1, + name: 'New york', + country: { + id: 2, + name: 'USA' + } + }; + + expect(controller.supplier.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.supplier.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.supplier.provinceFk).toEqual(1); + expect(controller.supplier.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.supplier.city).toEqual('New York'); + expect(controller.supplier.provinceFk).toEqual(1); + expect(controller.supplier.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 2b7d73541..1f5879370 100644 --- a/modules/supplier/front/index.js +++ b/modules/supplier/front/index.js @@ -5,6 +5,7 @@ import './card'; import './descriptor'; import './index/'; import './search-panel'; -import './log'; import './summary'; +import './fiscal-data'; import './contact'; +import './log'; diff --git a/modules/supplier/front/routes.json b/modules/supplier/front/routes.json index d679dd979..4dd23c2b3 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"}, {"state": "supplier.card.contact", "icon": "contact_phone"}, {"state": "supplier.card.log", "icon": "history"} ] @@ -41,8 +43,24 @@ "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" + }, + "acl": ["administrative"] + }, { "url" : "/log", "state": "supplier.card.log", "component": "vn-supplier-log",