diff --git a/client/client/routes.json b/client/client/routes.json index 896ab0cad..d8d0fbc03 100644 --- a/client/client/routes.json +++ b/client/client/routes.json @@ -9,16 +9,19 @@ "state": "clients", "component": "vn-client-index", "acl": ["employee"] - }, { + }, + { "url": "/create", "state": "create", "component": "vn-client-create" - }, { + }, + { "url": "/clients/:id", "state": "clientCard", "abstract": true, "component": "vn-client-card" - }, { + }, + { "url": "/basic-data", "state": "clientCard.basicData", "component": "vn-client-basic-data", @@ -29,7 +32,8 @@ "description": "Basic data", "icon": "person" } - }, { + }, + { "url": "/fiscal-data", "state": "clientCard.fiscalData", "component": "vn-client-fiscal-data", @@ -40,7 +44,8 @@ "description": "Fiscal data", "icon": "account_balance" } - }, { + }, + { "url": "/billing-data", "state": "clientCard.billingData", "component": "vn-client-billing-data", @@ -51,12 +56,14 @@ "description": "Pay method", "icon": "assignment" } - }, { + }, + { "url": "/addresses", "state": "clientCard.addresses", "component": "ui-view", "abstract": true - }, { + }, + { "url": "/list", "state": "clientCard.addresses.list", "component": "vn-client-addresses", @@ -67,15 +74,18 @@ "description": "Addresses", "icon": "local_shipping" } - }, { + }, + { "url": "/create", "state": "clientCard.addresses.create", "component": "vn-address-create" - }, { + }, + { "url": "/:addressId/edit", "state": "clientCard.addresses.edit", "component": "vn-address-edit" - }, { + }, + { "url": "/web-access", "state": "clientCard.webAccess", "component": "vn-client-web-access", @@ -86,12 +96,14 @@ "description": "Web access", "icon": "language" } - }, { + }, + { "url": "/notes", "state": "clientCard.notes", "component": "ui-view", "abstract": true - }, { + }, + { "url": "/list", "state": "clientCard.notes.list", "component": "vn-client-notes", @@ -102,16 +114,19 @@ "description": "Notes", "icon": "insert_drive_file" } - }, { + }, + { "url": "/create", "state": "clientCard.notes.create", "component": "vn-note-create" - }, { + }, + { "url": "/credit", "abstract": true, "state": "clientCard.credit", "component": "ui-view" - }, { + }, + { "url": "/list", "state": "clientCard.credit.list", "component": "vn-client-credit-list", @@ -123,19 +138,22 @@ "icon": "credit_card" }, "acl": ["manager", "salesAssistant", "teamBoss", "teamManager"] - }, { + }, + { "url": "/create", "state": "clientCard.credit.create", "component": "vn-client-credit-create", "params": { "client": "$ctrl.client" } - }, { + }, + { "url": "/greuge", "abstract": true, "state": "clientCard.greuge", "component": "ui-view" - }, { + }, + { "url": "/list", "state": "clientCard.greuge.list", "component": "vn-client-greuge-list", @@ -146,14 +164,16 @@ "description": "Greuge", "icon": "work" } - }, { + }, + { "url": "/create", "state": "clientCard.greuge.create", "component": "vn-client-greuge-create", "params": { "client": "$ctrl.client" } - }, { + }, + { "url": "/mandate", "state": "clientCard.mandate", "component": "vn-client-mandate", diff --git a/client/item/routes.json b/client/item/routes.json index 863a8a4b9..127c1e84f 100644 --- a/client/item/routes.json +++ b/client/item/routes.json @@ -15,16 +15,19 @@ "state": "item.index", "component": "vn-item-list", "acl": ["developer"] - }, { + }, + { "url": "/create", "state": "item.create", "component": "vn-item-create" - }, { + }, + { "url": "/:id", "state": "item.card", "abstract": true, "component": "vn-item-card" - }, { + }, + { "url" : "/data", "state": "item.card.data", "component": "vn-item-data", @@ -35,7 +38,8 @@ "description": "Basic data", "icon": "folder" } - },{ + }, + { "url" : "/tags", "state": "item.card.tags", "component": "vn-item-tags", @@ -46,7 +50,8 @@ "description": "Tags", "icon": "folder" } - },{ + }, + { "url" : "/history", "state": "item.card.history", "component": "vn-item-history", @@ -57,7 +62,8 @@ "description": "History", "icon": "folder" } - },{ + }, + { "url" : "/niche", "state": "item.card.niche", "component": "vn-item-niche", @@ -68,7 +74,8 @@ "description": "Niche", "icon": "folder" } - },{ + }, + { "url" : "/botanical", "state": "item.card.botanical", "component": "vn-item-botanical", @@ -81,7 +88,7 @@ } },{ "url" : "/barcode", - "state": "item.card.barcode", + "state": "item.card.itemBarcode", "component": "vn-item-barcode", "params": { "item": "$ctrl.item" diff --git a/client/item/src/barcode/item-barcode.html b/client/item/src/barcode/item-barcode.html index 549ac04d1..9f01f4c64 100644 --- a/client/item/src/barcode/item-barcode.html +++ b/client/item/src/barcode/item-barcode.html @@ -1,5 +1,32 @@ - - - Item barcode - - \ No newline at end of file +
+ + + + Item Barcodes + + + + + + + + + + + + + + + +
diff --git a/client/item/src/barcode/item-barcode.js b/client/item/src/barcode/item-barcode.js index 850c262b8..eac4ab6cb 100644 --- a/client/item/src/barcode/item-barcode.js +++ b/client/item/src/barcode/item-barcode.js @@ -1,5 +1,127 @@ import ngModule from '../module'; +export default class Controller { + constructor($state, $scope, $http, $q, $translate, vnApp) { + this.$state = $state; + this.$scope = $scope; + this.$http = $http; + this.$q = $q; + this.$translate = $translate; + this.vnApp = vnApp; + + this.barcodes = []; + this.removedBarcodes = []; + this.oldBarcodes = {}; + } + _setIconAdd() { + if (this.barcodes.length) { + this.barcodes.map(element => { + element.showAddIcon = false; + return true; + }); + this.barcodes[this.barcodes.length - 1].showAddIcon = true; + } else { + this.addBarcode(); + } + } + + _setDirtyForm() { + if (this.$scope.form) { + this.$scope.form.$setDirty(); + } + } + _unsetDirtyForm() { + if (this.$scope.form) { + this.$scope.form.$setPristine(); + } + } + _equalBarcodes(oldBarcode, newBarcode) { + return oldBarcode.id === newBarcode.id && oldBarcode.code === newBarcode.code; + } + + addBarcode() { + this.barcodes.push({code: null, itemFk: this.$state.params.id, showAddIcon: true}); + this._setIconAdd(); + } + + removeBarcode(index) { + let item = this.barcodes[index]; + if (item) { + this.barcodes.splice(index, 1); + this._setIconAdd(); + if (item.id) { + this.removedBarcodes.push(item.id); + this._setDirtyForm(); + } + } + } + + submit() { + let codes = []; + let repeatedBarcodes = false; + let canSubmit; + let barcodesObj = { + delete: this.removedBarcodes, + create: [], + update: [] + }; + for (let i = 0; i < this.barcodes.length; i++) { + let barcode = this.barcodes[i]; + let isNewBarcode = barcode.id === undefined; + + if (barcode.code && codes.indexOf(barcode.code) !== -1) { + repeatedBarcodes = true; + break; + } + if (barcode.code) codes.push(barcode.code); + + if (isNewBarcode && barcode.code) { + barcodesObj.create.push(barcode); + } else if (!isNewBarcode && !this._equalBarcodes(this.oldBarcodes[barcode.id], barcode)) { + barcodesObj.update.push(barcode); + } + } + + if (repeatedBarcodes) { + return this.vnApp.showMessage(this.$translate.instant('The barcode must be unique')); + } + + canSubmit = barcodesObj.update.length > 0 || barcodesObj.create.length > 0 || barcodesObj.delete.length > 0; + + if (canSubmit) { + return this.$http.post(`/item/api/ItemBarcodes/crudItemBarcodes`, barcodesObj).then(() => { + this.getBarcodes(); + this._unsetDirtyForm(); + }); + } + this.vnApp.showMessage(this.$translate.instant('No changes to save')); + } + + setOldBarcodes(response) { + this._setIconAdd(); + response.data.forEach(barcode => { + this.oldBarcodes[barcode.id] = Object.assign({}, barcode); + }); + } + + getBarcodes() { + let filter = { + where: {itemFk: this.$state.params.id} + }; + this.$http.get(`/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`).then(response => { + this.barcodes = response.data; + this.setOldBarcodes(response); + }); + } + + $onInit() { + this.getBarcodes(); + } +} + +Controller.$inject = ['$state', '$scope', '$http', '$q', '$translate', 'vnApp']; + ngModule.component('vnItemBarcode', { - template: require('./item-barcode.html') + template: require('./item-barcode.html'), + controller: Controller }); diff --git a/client/item/src/barcode/item-barcode.spec.js b/client/item/src/barcode/item-barcode.spec.js new file mode 100644 index 000000000..98e645c26 --- /dev/null +++ b/client/item/src/barcode/item-barcode.spec.js @@ -0,0 +1,142 @@ +import './item-barcode.js'; + +describe('Item', () => { + describe('Component vnItemBarcode', () => { + let $componentController; + let $state; + let controller; + let $httpBackend; + + beforeEach(() => { + angular.mock.module('item'); + }); + + beforeEach(angular.mock.inject((_$componentController_, _$state_, _$httpBackend_) => { + $componentController = _$componentController_; + $state = _$state_; + $httpBackend = _$httpBackend_; + $state.params.id = '1'; + controller = $componentController('vnItemBarcode', {$state: $state}); + })); + + describe('add / remove barcode()', () => { + it('should add one empty barcode into controller barcodes collection and call _setIconAdd()', () => { + controller.barcodes = []; + spyOn(controller, '_setIconAdd').and.callThrough(); + controller.addBarcode(); + + expect(controller._setIconAdd).toHaveBeenCalledWith(); + expect(controller.barcodes.length).toEqual(1); + expect(controller.barcodes[0].id).toBe(undefined); + expect(controller.barcodes[0].showAddIcon).toBeTruthy(); + }); + + it('should remove a barcode that occupies the position in the index given and call _setIconAdd()', () => { + let index = 2; + controller.barcodes = [ + {id: 1, code: '1111', showAddIcon: false}, + {id: 2, code: '2222', showAddIcon: false}, + {id: 3, code: '3333', showAddIcon: true} + ]; + + spyOn(controller, '_setIconAdd').and.callThrough(); + + controller.removeBarcode(index); + + expect(controller._setIconAdd).toHaveBeenCalledWith(); + expect(controller.barcodes.length).toEqual(2); + expect(controller.barcodes[0].showAddIcon).toBeFalsy(); + expect(controller.barcodes[1].showAddIcon).toBeTruthy(); + expect(controller.barcodes[index]).toBe(undefined); + }); + }); + + describe('_equalBarcodes()', () => { + it('should return true if two barcodes are equals independent of control attributes', () => { + let code1 = {id: 1, code: '1111', showAddIcon: true}; + let code2 = {id: 1, code: '1111', showAddIcon: false}; + let equals = controller._equalBarcodes(code2, code1); + + expect(equals).toBeTruthy(); + }); + + it('should return false if two barcodes aint equals independent of control attributes', () => { + let code1 = {id: 1, code: '1111', showAddIcon: true}; + let code2 = {id: 1, code: '2222', showAddIcon: true}; + let equals = controller._equalBarcodes(code2, code1); + + expect(equals).toBeFalsy(); + }); + }); + + describe('getBarcodes()', () => { + it('should perform a GET query to receive the item barcodes', () => { + let filter = { + where: {itemFk: '1'} + }; + let res = [{id: 1, code: '1111'}]; + $httpBackend.when('GET', `/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`).respond(res); + $httpBackend.expectGET(`/item/api/ItemBarcodes?filter=${JSON.stringify(filter)}`); + controller.getBarcodes(); + $httpBackend.flush(); + }); + }); + + describe('submit()', () => { + it("should return an error message 'The barcode must be unique' when the code isnt unique", () => { + spyOn(controller.vnApp, 'showMessage').and.callThrough(); + controller.barcodes = [ + {code: 123454, itemFk: 1, id: 1}, + {code: 123454, itemFk: 1} + ]; + controller.oldBarcodes = {1: {id: 1, code: 123454, itemFk: 1}}; + controller.submit(); + + expect(controller.vnApp.showMessage).toHaveBeenCalledWith('The barcode must be unique'); + }); + + it("should perfom a query to delete barcodes", () => { + controller.removedBarcodes = [1, 2, 3]; + + $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{code: 123454, itemFk: 1, id: 1}]); + $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!'); + $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`); + controller.submit(); + $httpBackend.flush(); + }); + + it("should perfom a query to update barcodes", () => { + controller.barcodes = [{code: 2222, itemFk: 1, id: 1}]; + controller.oldBarcodes = {1: {id: 1, code: 1111, itemFk: 1}}; + + $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{}]); + $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!'); + $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`); + controller.submit(); + $httpBackend.flush(); + }); + + it("should perfom a query to create new barcode", () => { + controller.barcodes = [{code: 1111, itemFk: 1}]; + + $httpBackend.when('GET', `/item/api/ItemBarcodes?filter={"where":{"itemFk":"1"}}`).respond([{}]); + $httpBackend.when('POST', `/item/api/ItemBarcodes/crudItemBarcodes`).respond('ok!'); + $httpBackend.expectPOST(`/item/api/ItemBarcodes/crudItemBarcodes`); + controller.submit(); + $httpBackend.flush(); + }); + + it("should return a message 'No changes to save' when there are no changes to apply", () => { + spyOn(controller.vnApp, 'showMessage').and.callThrough(); + controller.oldBarcodes = [ + {code: 1, itemFk: 1, id: 1}, + {code: 2, itemFk: 1, id: 2} + ]; + controller.barcodes = []; + controller.submit(); + + expect(controller.vnApp.showMessage).toHaveBeenCalledWith('No changes to save'); + }); + }); + }); +}); diff --git a/gulpfile.js b/gulpfile.js index 05bcc6b76..af1ecc636 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,6 +76,22 @@ gulp.task('e2e-only', () => { .pipe(jasmine({reporter: 'none'})); }); +/** + * Runs the backend tests. + */ +// gulp.task('test', ['test-only'], async () => { +// gulp.watch('./services/**/*', ['test-only']); +// gulp.unwatch('./services/node_modules'); +// }); + +// gulp.task('test-only', () => { +// const jasmine = require('gulp-jasmine'); +// gulp.src('./services/loopback/common/**/*[sS]pec.js') +// .pipe(jasmine( +// require('./services-test.config') +// )); +// }); + /** * Cleans all generated project files. */ @@ -181,7 +197,7 @@ gulp.task('nginx-start', ['nginx-conf'], async () => { if (isWindows) nginxBin = `start /B ${nginxBin}`; - log(`Application will be at http://${proxyConf.host}:${proxyConf.port}/`); + log(`Application will be available at http://${proxyConf.host}:${proxyConf.port}/`); await execP(`${nginxBin} -c "${nginxConf}" -p "${nginxDir}"`); }); diff --git a/services-test.config.js b/services-test.config.js new file mode 100644 index 000000000..aa8866fc8 --- /dev/null +++ b/services-test.config.js @@ -0,0 +1,24 @@ +// const SpecReporter = require('jasmine-spec-reporter').SpecReporter; + +// module.exports = { +// reporter: new SpecReporter({ +// spec: { +// // displayStacktrace: 'summary', +// displaySuccessful: false, +// displayFailedSpec: true, +// displaySpecDuration: true +// } +// }), +// config: { +// spec_dir: 'services', +// spec_files: [ +// // '**/*.spec.js', +// 'auth/server/**/*.spec.js', +// 'client/common/**/*.spec.js', +// 'loopback/common/**/*.spec.js' +// ], +// helpers: [ +// '/services/utils/jasmineHelpers.js' +// ] +// } +// }; diff --git a/services/client/common/methods/address/crudAddressObservations.js b/services/client/common/methods/address/crudAddressObservations.js index b158b0749..0b80d1bac 100644 --- a/services/client/common/methods/address/crudAddressObservations.js +++ b/services/client/common/methods/address/crudAddressObservations.js @@ -11,9 +11,6 @@ module.exports = Self => { http: {source: 'body'} } ], - returns: { - arg: 'sumAmount' - }, http: { path: `/crudAddressObservations`, verb: 'post' diff --git a/services/item/common/methods/item/crudItemBarcodes.js b/services/item/common/methods/item/crudItemBarcodes.js new file mode 100644 index 000000000..93eedf133 --- /dev/null +++ b/services/item/common/methods/item/crudItemBarcodes.js @@ -0,0 +1,36 @@ +module.exports = Self => { + Self.remoteMethod('crudItemBarcodes', { + description: 'create, update or delete barcodes', + accessType: 'WRITE', + accepts: [ + { + arg: 'barcodes', + type: 'Object', + require: true, + description: 'object with barcodes to create, update or delete, Example: {create: [], update: [], delete: []}', + http: {source: 'body'} + } + ], + http: { + path: `/crudItemBarcodes`, + verb: 'post' + } + }); + + Self.crudItemBarcodes = barcodes => { + let promises = []; + + if (barcodes.delete && barcodes.delete.length) { + promises.push(Self.destroyAll({id: {inq: barcodes.delete}})); + } + if (barcodes.create.length) { + promises.push(Self.create(barcodes.create)); + } + if (barcodes.update.length) { + barcodes.update.forEach(barcode => { + promises.push(Self.upsert(barcode)); + }); + } + return Promise.all(promises); + }; +}; diff --git a/services/item/common/methods/item/specs/crudItemBarcodes.spec.js b/services/item/common/methods/item/specs/crudItemBarcodes.spec.js new file mode 100644 index 000000000..72365229a --- /dev/null +++ b/services/item/common/methods/item/specs/crudItemBarcodes.spec.js @@ -0,0 +1,51 @@ +const crudItemBarcodes = require('../crudItemBarcodes'); +const catchErrors = require('../../../../../../services/utils/jasmineHelpers').catchErrors; + +describe('Item crudItemBarcodes()', () => { + it('should call the destroyAll methodif there are ids in delete Array', done => { + let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']); + + crudItemBarcodes(self); + self.crudItemBarcodes({ + delete: [1], + create: [], + update: [] + }).then(result => { + expect(self.destroyAll).toHaveBeenCalledWith({id: {inq: [1]}}); + done(); + }) + .catch(catchErrors(done)); + }); + + it('should call the create method if there are ids in create Array', done => { + let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']); + + crudItemBarcodes(self); + self.crudItemBarcodes({ + delete: [], + create: [1], + update: [] + }).then(result => { + expect(self.create).toHaveBeenCalledWith([1]); + done(); + }) + .catch(catchErrors(done)); + }); + + it('should call the upsert method as many times as ids in update Array', done => { + let self = jasmine.createSpyObj('self', ['remoteMethod', 'crudItemBarcodes', 'destroyAll', 'create', 'upsert']); + + crudItemBarcodes(self); + self.crudItemBarcodes({ + delete: [], + create: [], + update: [1, 2] + }).then(result => { + expect(self.upsert).toHaveBeenCalledWith(1); + expect(self.upsert).toHaveBeenCalledWith(2); + expect(self.upsert.calls.count()).toEqual(2); + done(); + }) + .catch(catchErrors(done)); + }); +}); diff --git a/services/item/common/models/item.json b/services/item/common/models/item.json index 5598f371d..a4121e8f5 100644 --- a/services/item/common/models/item.json +++ b/services/item/common/models/item.json @@ -79,6 +79,11 @@ "type": "hasMany", "model": "ItemTag", "foreignKey": "itemFk" + }, + "itemBarcode": { + "type": "hasMany", + "model": "ItemBarcode", + "foreignKey": "itemFk" } } } \ No newline at end of file diff --git a/services/item/common/models/itemBarcode.js b/services/item/common/models/itemBarcode.js new file mode 100644 index 000000000..be746ecb3 --- /dev/null +++ b/services/item/common/models/itemBarcode.js @@ -0,0 +1,3 @@ +module.exports = function(Self) { + require('../methods/item/crudItemBarcodes.js')(Self); +}; diff --git a/services/item/common/models/itemBarcode.json b/services/item/common/models/itemBarcode.json new file mode 100644 index 000000000..9274cae98 --- /dev/null +++ b/services/item/common/models/itemBarcode.json @@ -0,0 +1,28 @@ +{ + "name": "ItemBarcode", + "base": "VnModel", + "options": { + "mysql": { + "table": "itemBarcode", + "database": "vn" + } + }, + "properties": { + "id": { + "type": "Number", + "id": true, + "description": "Identifier" + }, + "code": { + "type": "String", + "required": true + } + }, + "relations": { + "item": { + "type": "belongsTo", + "model": "Item", + "foreignKey": "itemFk" + } + } + } diff --git a/services/item/server/model-config.json b/services/item/server/model-config.json index 09eed1735..8c1e5d26b 100644 --- a/services/item/server/model-config.json +++ b/services/item/server/model-config.json @@ -37,5 +37,8 @@ }, "ItemLog": { "dataSource": "vn" + }, + "ItemBarcode": { + "dataSource": "vn" } } diff --git a/services_tests.js b/services_tests.js index 068f8c8f2..fe3c907da 100644 --- a/services_tests.js +++ b/services_tests.js @@ -19,6 +19,7 @@ jasmine.loadConfig({ spec_files: [ 'auth/server/**/*[sS]pec.js', 'client/common/**/*[sS]pec.js', + 'item/common/**/*[sS]pec.js', 'loopback/common/**/*[sS]pec.js' ], helpers: [