diff --git a/db/changes/10310-mothersDay/00-supplierAddress.sql b/db/changes/10310-mothersDay/00-supplierAddress.sql new file mode 100644 index 000000000..e15a25a81 --- /dev/null +++ b/db/changes/10310-mothersDay/00-supplierAddress.sql @@ -0,0 +1,18 @@ +CREATE TABLE `vn`.`supplierAddress` +( + id INT NULL AUTO_INCREMENT, + supplierFk INT NULL, + nickname VARCHAR(40) NULL, + street VARCHAR(255) NULL, + provinceFk SMALLINT(6) UNSIGNED NULL, + postalCode VARCHAR(10) NULL, + city VARCHAR(50) NULL, + phone VARCHAR(15) NULL, + mobile VARCHAR(15) NULL, + CONSTRAINT supplierAddress_pk + PRIMARY KEY (id), + CONSTRAINT supplierAddress_province_fk + FOREIGN KEY (provinceFk) REFERENCES province (id) + ON UPDATE CASCADE +); + diff --git a/modules/client/front/address/index/index.html b/modules/client/front/address/index/index.html index 51b310128..ebdc44724 100644 --- a/modules/client/front/address/index/index.html +++ b/modules/client/front/address/index/index.html @@ -8,8 +8,8 @@ @@ -26,7 +26,7 @@ ui-sref="client.card.address.edit({addressId: {{::address.id}}})" class="vn-pa-sm border-solid border-radius" ng-class="{'item-disabled': !address.isActive}" - translate-attr="{title: 'Edit address'}"> + translate-attr="{title: 'Edit consignee'}"> @@ -78,7 +78,7 @@ diff --git a/modules/client/front/address/locale/es.yml b/modules/client/front/address/locale/es.yml index 06d9e76f7..e1383fab5 100644 --- a/modules/client/front/address/locale/es.yml +++ b/modules/client/front/address/locale/es.yml @@ -1,8 +1,8 @@ # Index Set as default: Establecer como predeterminado Active first to set as default: Active primero para marcar como predeterminado -Search by address: Buscar por consignatario -You can search by address id or name: Puedes buscar por el id o nombre del consignatario +Search by consignee: Buscar por consignatario +You can search by consignee id or name: Puedes buscar por el id o nombre del consignatario # Edit Enabled: Activo Is equalizated: Recargo de equivalencia diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml index 82cbb129e..1a5a570a7 100644 --- a/modules/client/front/locale/es.yml +++ b/modules/client/front/locale/es.yml @@ -36,9 +36,9 @@ Clients: Clientes New client: Nuevo cliente Fiscal data: Datos fiscales Billing data: Forma de pago -Addresses: Consignatarios -New address: Nuevo consignatario -Edit address: Editar consignatario +Consignees: Consignatarios +New consignee: Nuevo consignatario +Edit consignee: Editar consignatario Web access: Acceso web Notes: Notas New note: Nueva nota diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json index a2d559645..4bd4086e0 100644 --- a/modules/client/front/routes.json +++ b/modules/client/front/routes.json @@ -102,7 +102,7 @@ "url": "/index?q", "state": "client.card.address.index", "component": "vn-client-address-index", - "description": "Addresses", + "description": "Consignees", "params": { "client": "$ctrl.client" } diff --git a/modules/supplier/back/methods/supplier/createAddress.js b/modules/supplier/back/methods/supplier/createAddress.js new file mode 100644 index 000000000..63995ba8f --- /dev/null +++ b/modules/supplier/back/methods/supplier/createAddress.js @@ -0,0 +1,90 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = function(Self) { + Self.remoteMethodCtx('createAddress', { + description: 'Creates a supplier address', + accepts: [{ + arg: 'id', + type: 'number', + description: 'The supplier id', + http: {source: 'path'} + }, + { + arg: 'nickname', + type: 'string', + required: true + }, + { + arg: 'city', + type: 'string', + required: true + }, + { + arg: 'street', + type: 'string', + required: true + }, + { + arg: 'phone', + type: 'string' + }, + { + arg: 'mobile', + type: 'string' + }, + { + arg: 'postalCode', + type: 'string' + }, + { + arg: 'provinceFk', + type: 'number' + }], + returns: { + root: true, + type: 'Object' + }, + http: { + verb: 'post', + path: '/:id/createAddress' + } + }); + + Self.createAddress = async(ctx, clientFk) => { + const models = Self.app.models; + const args = ctx.args; + const tx = await models.Address.beginTransaction({}); + + try { + const options = {transaction: tx}; + const province = await models.Province.findById(args.provinceFk, { + include: { + relation: 'country' + } + }, options); + + const isUeeMember = province.country().isUeeMember; + if (!isUeeMember && !args.incotermsFk) + throw new UserError(`Incoterms is required for a non UEE member`); + + if (!isUeeMember && !args.customsAgentFk) + throw new UserError(`Customs agent is required for a non UEE member`); + + delete args.ctx; // Remove unwanted properties + const newAddress = await models.Address.create(args, options); + const client = await Self.findById(clientFk, null, options); + + if (args.isDefaultAddress) { + await client.updateAttributes({ + defaultAddressFk: newAddress.id + }, options); + } + + await tx.commit(); + return newAddress; + } catch (e) { + await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/supplier/back/model-config.json b/modules/supplier/back/model-config.json index 62c580aca..7febc17b4 100644 --- a/modules/supplier/back/model-config.json +++ b/modules/supplier/back/model-config.json @@ -1,8 +1,14 @@ { + "PayDem": { + "dataSource": "vn" + }, "Supplier": { "dataSource": "vn" }, - "PayDem": { + "SupplierAddress": { + "dataSource": "vn" + }, + "SupplierAccount": { "dataSource": "vn" }, "SupplierLog": { @@ -10,8 +16,5 @@ }, "SupplierContact": { "dataSource": "vn" - }, - "SupplierAccount": { - "dataSource": "vn" } } diff --git a/modules/supplier/back/models/supplier-address.js b/modules/supplier/back/models/supplier-address.js new file mode 100644 index 000000000..ca08fa719 --- /dev/null +++ b/modules/supplier/back/models/supplier-address.js @@ -0,0 +1,16 @@ +module.exports = Self => { + Self.validateAsync('postalCode', hasValidPostcode, { + message: `The postcode doesn't exist. Please enter a correct one` + }); + + async function hasValidPostcode(err, done) { + if (!this.postalCode) + return done(); + + const models = Self.app.models; + const postcode = await models.Postcode.findById(this.postalCode); + + if (!postcode) err(); + done(); + } +}; diff --git a/modules/supplier/back/models/supplier-address.json b/modules/supplier/back/models/supplier-address.json new file mode 100644 index 000000000..d1cee2d50 --- /dev/null +++ b/modules/supplier/back/models/supplier-address.json @@ -0,0 +1,55 @@ +{ + "name": "SupplierAddress", + "description": "Supplier addresses", + "base": "Loggable", + "log": { + "model": "SupplierLog", + "relation": "supplier", + "showField": "name" + }, + "options": { + "mysql": { + "table": "supplierAddress" + } + }, + "properties": { + "id": { + "type": "Number", + "id": true, + "description": "Identifier" + }, + "nickname": { + "type": "string", + "required": true + }, + "street": { + "type": "string", + "required": true + }, + "city": { + "type": "string", + "required": true + }, + "postalCode": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "mobile": { + "type": "string" + } + }, + "relations": { + "province": { + "type": "belongsTo", + "model": "Province", + "foreignKey": "provinceFk" + }, + "supplier": { + "type": "belongsTo", + "model": "Supplier", + "foreignKey": "supplierFk" + } + } +} \ No newline at end of file diff --git a/modules/supplier/back/models/supplier.json b/modules/supplier/back/models/supplier.json index 4ec568c8b..9567be785 100644 --- a/modules/supplier/back/models/supplier.json +++ b/modules/supplier/back/models/supplier.json @@ -149,6 +149,11 @@ "type": "belongsTo", "model": "SageWithholding", "foreignKey": "sageWithholdingFk" + }, + "addresses": { + "type": "hasMany", + "model": "SupplierAddress", + "foreignKey": "supplierFk" } }, "acls": [ diff --git a/modules/supplier/front/address/create/index.html b/modules/supplier/front/address/create/index.html new file mode 100644 index 000000000..4d66b70f0 --- /dev/null +++ b/modules/supplier/front/address/create/index.html @@ -0,0 +1,109 @@ + + + + +
+ + + + + + + + + + + {{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/address/create/index.js b/modules/supplier/front/address/create/index.js new file mode 100644 index 000000000..21b845881 --- /dev/null +++ b/modules/supplier/front/address/create/index.js @@ -0,0 +1,74 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.address = { + supplierFk: this.$params.id + }; + } + + onSubmit() { + this.$.watcher.submit().then(res => { + this.$state.go('supplier.card.address.index'); + }); + } + + get town() { + return this._town; + } + + // Town auto complete + set town(selection) { + this._town = selection; + + if (!selection) return; + + const province = selection.province; + const postcodes = selection.postcodes; + + if (!this.address.provinceFk) + this.address.provinceFk = province.id; + + if (postcodes.length === 1) + this.address.postalCode = postcodes[0].code; + } + + get postcode() { + return this._postcode; + } + + // Postcode auto complete + set postcode(selection) { + this._postcode = selection; + + if (!selection) return; + + const town = selection.town; + const province = town.province; + + if (!this.address.city) + this.address.city = town.name; + + if (!this.address.provinceFk) + this.address.provinceFk = province.id; + } + + onResponse(response) { + this.address.postalCode = response.code; + this.address.city = response.city; + this.address.provinceFk = response.provinceFk; + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnSupplierAddressCreate', { + template: require('./index.html'), + controller: Controller, + bindings: { + supplier: '<' + } +}); diff --git a/modules/supplier/front/address/create/index.spec.js b/modules/supplier/front/address/create/index.spec.js new file mode 100644 index 000000000..026de3769 --- /dev/null +++ b/modules/supplier/front/address/create/index.spec.js @@ -0,0 +1,102 @@ +import './index'; +import watcher from 'core/mocks/watcher'; + +describe('Supplier', () => { + describe('Component vnSupplierAddressCreate', () => { + let $scope; + let controller; + let $element; + let $state; + + beforeEach(ngModule('supplier')); + + beforeEach(inject(($componentController, $rootScope, _$state_) => { + $scope = $rootScope.$new(); + $state = _$state_; + $state.params.id = '1234'; + $element = angular.element(''); + controller = $componentController('vnSupplierAddressCreate', {$element, $scope}); + controller.$.watcher = watcher; + controller.$.watcher.submit = () => { + return { + then: callback => { + callback({data: {id: 124}}); + } + }; + }; + controller.supplier = {id: 1}; + })); + + describe('onSubmit()', () => { + it('should perform a PATCH and then redirect to the main section', () => { + jest.spyOn(controller.$state, 'go'); + controller.onSubmit(); + + expect(controller.$state.go).toHaveBeenCalledWith('supplier.card.address.index'); + }); + }); + + 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.address.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.address.provinceFk).toEqual(1); + expect(controller.address.postalCode).toEqual('46001'); + }); + }); + + describe('postcode() setter', () => { + it(`should set the town and province 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.address.city).toEqual('New York'); + expect(controller.address.provinceFk).toEqual(1); + }); + }); + }); +}); diff --git a/modules/supplier/front/address/edit/index.html b/modules/supplier/front/address/edit/index.html new file mode 100644 index 000000000..dd4cbb4d2 --- /dev/null +++ b/modules/supplier/front/address/edit/index.html @@ -0,0 +1,104 @@ + + + + + +
+ + + + + + + + + + + {{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/address/edit/index.js b/modules/supplier/front/address/edit/index.js new file mode 100644 index 000000000..4c7450666 --- /dev/null +++ b/modules/supplier/front/address/edit/index.js @@ -0,0 +1,62 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + this.$.watcher.submit() + .then(() => this.$state.go('supplier.card.address.index')); + } + + get town() { + return this._town; + } + + // Town auto complete + set town(selection) { + const oldValue = this._town; + this._town = selection; + + if (!selection || !oldValue) return; + + const province = selection.province; + const postcodes = selection.postcodes; + + if (!this.address.provinceFk) + this.address.provinceFk = province.id; + + if (!this.address.postalCode && postcodes.length === 1) + this.address.postalCode = 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; + + if (!this.address.city) + this.address.city = town.name; + + if (!this.address.provinceFk) + this.address.provinceFk = province.id; + } + + onResponse(response) { + this.address.postalCode = response.code; + this.address.city = response.city; + this.address.provinceFk = response.provinceFk; + } +} + +ngModule.vnComponent('vnSupplierAddressEdit', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/supplier/front/address/edit/index.spec.js b/modules/supplier/front/address/edit/index.spec.js new file mode 100644 index 000000000..991163baa --- /dev/null +++ b/modules/supplier/front/address/edit/index.spec.js @@ -0,0 +1,39 @@ +import './index'; +import watcher from 'core/mocks/watcher'; + +describe('Supplier', () => { + describe('Component vnSupplierAddressEdit', () => { + let $scope; + let controller; + let $element; + let $state; + + beforeEach(ngModule('supplier')); + + beforeEach(inject(($componentController, $rootScope, _$state_) => { + $scope = $rootScope.$new(); + $state = _$state_; + $state.params.addressId = '1'; + $element = angular.element(''); + controller = $componentController('vnSupplierAddressEdit', {$element, $scope}); + controller.address = {id: 1}; + controller.$.watcher = watcher; + controller.$.watcher.submit = () => { + return { + then: callback => { + callback({data: {id: 124}}); + } + }; + }; + })); + + describe('onSubmit()', () => { + it('should perform a PATCH and then redirect to the main section', () => { + jest.spyOn(controller.$state, 'go'); + controller.onSubmit(); + + expect(controller.$state.go).toHaveBeenCalledWith('supplier.card.address.index'); + }); + }); + }); +}); diff --git a/modules/supplier/front/address/index/index.html b/modules/supplier/front/address/index/index.html new file mode 100644 index 000000000..ad8f6f250 --- /dev/null +++ b/modules/supplier/front/address/index/index.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + diff --git a/modules/supplier/front/address/index/index.js b/modules/supplier/front/address/index/index.js new file mode 100644 index 000000000..c3985a0c1 --- /dev/null +++ b/modules/supplier/front/address/index/index.js @@ -0,0 +1,46 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + this.filter = { + fields: [ + 'id', + 'nickname', + 'street', + 'city', + 'provinceFk', + 'phone', + 'mobile', + 'postalCode' + ], + order: ['nickname ASC'], + include: [{ + relation: 'province', + scope: { + fields: ['id', 'name'] + } + }] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {nickname: {like: `%${value}%`}}; + } + } +} +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnSupplierAddressIndex', { + template: require('./index.html'), + controller: Controller, + bindings: { + supplier: '<' + } +}); diff --git a/modules/supplier/front/address/index/index.spec.js b/modules/supplier/front/address/index/index.spec.js new file mode 100644 index 000000000..086d3a9fa --- /dev/null +++ b/modules/supplier/front/address/index/index.spec.js @@ -0,0 +1,34 @@ +import './index'; + +describe('Supplier', () => { + describe('Component vnSupplierAddressIndex', () => { + let controller; + let $scope; + let $stateParams; + + beforeEach(ngModule('supplier')); + + beforeEach(inject(($componentController, $rootScope, _$stateParams_) => { + $stateParams = _$stateParams_; + $stateParams.id = 1; + $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnSupplierAddressIndex', {$element, $scope}); + controller.supplier = {id: 1}; + })); + + describe('exprBuilder()', () => { + it('should return a filter based on a search by id', () => { + const filter = controller.exprBuilder('search', '123'); + + expect(filter).toEqual({id: '123'}); + }); + + it('should return a filter based on a search by name', () => { + const filter = controller.exprBuilder('search', 'Arkham Chemicals'); + + expect(filter).toEqual({nickname: {like: '%Arkham Chemicals%'}}); + }); + }); + }); +}); diff --git a/modules/supplier/front/address/index/style.scss b/modules/supplier/front/address/index/style.scss new file mode 100644 index 000000000..44ce07b3c --- /dev/null +++ b/modules/supplier/front/address/index/style.scss @@ -0,0 +1,21 @@ +@import "variables"; +@import "./effects"; + +vn-supplier-address-index { + .address { + padding-bottom: $spacing-md; + + &:last-child { + padding-bottom: 0; + } + & > a { + @extend %clickable; + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + color: inherit; + overflow: hidden; + } + } +} \ No newline at end of file diff --git a/modules/supplier/front/address/locale/es.yml b/modules/supplier/front/address/locale/es.yml new file mode 100644 index 000000000..30009fa87 --- /dev/null +++ b/modules/supplier/front/address/locale/es.yml @@ -0,0 +1,18 @@ +# Index +Search by address: Buscar por dirección +You can search by address id or name: Puedes buscar por el id o nombre de la dirección + +# Create +Street address: Dirección postal +Postcode: Código postal +Town/City: Ciudad +Province: Provincia +Phone: Teléfono +Mobile: Móvil + +# Common +Fiscal name: Nombre fiscal +Street: Dirección fiscal +Addresses: Direcciones +New address: Nueva dirección +Edit address: Editar dirección \ No newline at end of file diff --git a/modules/supplier/front/index.js b/modules/supplier/front/index.js index 9c5cd4195..dc131ef4a 100644 --- a/modules/supplier/front/index.js +++ b/modules/supplier/front/index.js @@ -15,3 +15,6 @@ import './log'; import './consumption'; import './consumption-search-panel'; import './billing-data'; +import './address/index'; +import './address/create'; +import './address/edit'; diff --git a/modules/supplier/front/routes.json b/modules/supplier/front/routes.json index 5dc6a29b0..9a0dee48b 100644 --- a/modules/supplier/front/routes.json +++ b/modules/supplier/front/routes.json @@ -12,6 +12,7 @@ {"state": "supplier.card.basicData", "icon": "settings"}, {"state": "supplier.card.fiscalData", "icon": "account_balance"}, {"state": "supplier.card.billingData", "icon": "icon-payment"}, + {"state": "supplier.card.address.index", "icon": "icon-delivery"}, {"state": "supplier.card.account", "icon": "contact_support"}, {"state": "supplier.card.contact", "icon": "contact_phone"}, {"state": "supplier.card.log", "icon": "history"}, @@ -49,7 +50,8 @@ "params": { "supplier": "$ctrl.supplier" } - }, { + }, + { "url": "/basic-data", "state": "supplier.card.basicData", "component": "vn-supplier-basic-data", @@ -58,7 +60,8 @@ "params": { "supplier": "$ctrl.supplier" } - }, { + }, + { "url": "/fiscal-data", "state": "supplier.card.fiscalData", "component": "vn-supplier-fiscal-data", @@ -67,7 +70,8 @@ "supplier": "$ctrl.supplier" }, "acl": ["administrative"] - }, { + }, + { "url" : "/log", "state": "supplier.card.log", "component": "vn-supplier-log", @@ -100,7 +104,8 @@ "supplier": "$ctrl.supplier" }, "acl": ["administrative"] - },{ + }, + { "url": "/account", "state": "supplier.card.account", "component": "vn-supplier-account", @@ -109,6 +114,36 @@ "supplier": "$ctrl.supplier" }, "acl": ["administrative"] + }, + { + "url": "/address", + "state": "supplier.card.address", + "component": "ui-view", + "abstract": true + }, + { + "url": "/index?q", + "state": "supplier.card.address.index", + "component": "vn-supplier-address-index", + "description": "Addresses", + "params": { + "supplier": "$ctrl.supplier" + } + }, + { + "url": "/create", + "state": "supplier.card.address.create", + "component": "vn-supplier-address-create", + "description": "New address", + "params": { + "supplier": "$ctrl.supplier" + } + }, + { + "url": "/:addressId/edit", + "state": "supplier.card.address.edit", + "component": "vn-supplier-address-edit", + "description": "Edit address" } ] } \ No newline at end of file