Index: CHANGELOG.md =================================================================== diff -u -N -r19893230364f711560a71cbcbbcf38d0841c9fab -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- CHANGELOG.md (.../CHANGELOG.md) (revision 19893230364f711560a71cbcbbcf38d0841c9fab) +++ CHANGELOG.md (.../CHANGELOG.md) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -11,6 +11,7 @@ * [OLMIS-7471](https://openlmis.atlassian.net/browse/OLMIS-7471): Change terminology in button and messages from 'hide' to 'deactivate' * [OLMIS-7467](https://openlmis.atlassian.net/browse/OLMIS-7467): Move React base fields components to ui-components * [OLMIS-7463](https://openlmis.atlassian.net/browse/OLMIS-7463): Change filter on physical inventory for active/inactive items to checkbox +* [OLMIS-7458](https://openlmis.atlassian.net/browse/OLMIS-7458): Add new lots in receive and physical inventory screens 2.1.0 / 2021-10-28 ================== Index: src/stock-add-products-modal/add-products-modal.controller.js =================================================================== diff -u -N -r62d6207b74331fcc1259f62f2e0db386281e8fd8 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-add-products-modal/add-products-modal.controller.js (.../add-products-modal.controller.js) (revision 62d6207b74331fcc1259f62f2e0db386281e8fd8) +++ src/stock-add-products-modal/add-products-modal.controller.js (.../add-products-modal.controller.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -28,67 +28,131 @@ .module('stock-add-products-modal') .controller('AddProductsModalController', controller); - controller.$inject = ['items', 'hasLot', 'messageService', - 'modalDeferred', 'orderableGroupService', '$scope', 'MAX_INTEGER_VALUE']; + controller.$inject = ['availableItems', 'messageService', 'modalDeferred', 'orderableGroupService', + '$scope', 'MAX_INTEGER_VALUE', 'hasPermissionToAddNewLot', 'selectedItems', 'alertService', + 'moment']; - function controller(items, hasLot, messageService, - modalDeferred, orderableGroupService, $scope, MAX_INTEGER_VALUE) { + function controller(availableItems, messageService, modalDeferred, orderableGroupService, + $scope, MAX_INTEGER_VALUE, hasPermissionToAddNewLot, selectedItems, alertService, + moment) { var vm = this; + vm.$onInit = onInit; + vm.orderableSelectionChanged = orderableSelectionChanged; + vm.addOneProduct = addOneProduct; + vm.removeAddedProduct = removeAddedProduct; + vm.validate = validate; + vm.confirm = confirm; + vm.lotChanged = lotChanged; + vm.expirationDateChanged = expirationDateChanged; + vm.newLotCodeChanged = newLotCodeChanged; + /** * @ngdoc property * @propertyOf stock-add-products-modal.controller:AddProductsModalController - * @name items + * @name availableItems * @type {Array} * * @description * All products available for users to choose from. */ - vm.items = items; + vm.availableItems = undefined; /** * @ngdoc property * @propertyOf stock-add-products-modal.controller:AddProductsModalController - * @name hasLot + * @name addedItems * @type {Array} * * @description * Indicates if any line item has lot. If all line items have not lot, page will not display * any lot related information. */ - vm.hasLot = hasLot; + vm.addedItems = undefined; + /** * @ngdoc property * @propertyOf stock-add-products-modal.controller:AddProductsModalController - * @name addedItems - * @type {Array} + * @name selectedOrderableHasLots + * @type {boolean} * * @description - * Products that users have chosen in this modal. + * True if selected orderable has lots defined. */ - vm.addedItems = []; + vm.selectedOrderableHasLots = undefined; /** + * @ngdoc property + * @propertyOf stock-add-products-modal.controller:AddProductsModalController + * @name newLot + * @type {Object} + * + * @description + * Holds new lot object. + */ + vm.newLot = undefined; + + /** + * @ngdoc property + * @propertyOf stock-add-products-modal.controller:AddProductsModalController + * @name hasPermissionToAddNewLot + * @type {boolean} + * + * @description + * True when user has permission to add new lots. + */ + vm.hasPermissionToAddNewLot = undefined; + + /** * @ngdoc method * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name $onInit + * + * @description + * Initialization method of the AddProductsModalController. + */ + function onInit() { + vm.orderableGroups = orderableGroupService.groupByOrderableId(availableItems); + vm.availableItems = availableItems; + vm.addedItems = []; + vm.selectedOrderableHasLots = false; + vm.hasPermissionToAddNewLot = hasPermissionToAddNewLot; + vm.canAddNewLot = false; + + initiateNewLotObject(); + + modalDeferred.promise.catch(function() { + vm.addedItems.forEach(function(item) { + item.quantity = undefined; + item.quantityInvalid = undefined; + }); + }); + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController * @name orderableSelectionChanged * * @description * Reset form status and change content inside lots drop down list. */ - vm.orderableSelectionChanged = function() { + function orderableSelectionChanged() { //reset selected lot, so that lot field has no default value vm.selectedLot = null; + vm.canAddNewLot = false; + initiateNewLotObject(); + //same as above $scope.productForm.$setUntouched(); - //make form good as new, so errors won't persist $scope.productForm.$setPristine(); - vm.lots = orderableGroupService.lotsOf(vm.selectedOrderableGroup); + vm.lots = orderableGroupService.lotsOf(vm.selectedOrderableGroup, vm.hasPermissionToAddNewLot); + vm.selectedOrderableHasLots = vm.lots.length > 0; - }; + } /** * @ngdoc method @@ -98,30 +162,117 @@ * @description * Add the currently selected product into the table beneath it for users to do further actions. */ - vm.addOneProduct = function() { - var selectedItem = orderableGroupService - .findByLotInOrderableGroup(vm.selectedOrderableGroup, vm.selectedLot); + function addOneProduct() { + var selectedItem; - var notAlreadyAdded = selectedItem && !_.contains(vm.addedItems, selectedItem); - if (notAlreadyAdded) { - selectedItem.active = true; + if (vm.selectedOrderableGroup && vm.selectedOrderableGroup.length) { + vm.newLot.tradeItemId = vm.selectedOrderableGroup[0].orderable.identifiers.tradeItem; + } + if (vm.newLot.lotCode) { + selectedItem = orderableGroupService + .addItemWithNewLot(vm.newLot, vm.selectedOrderableGroup[0]); + } else { + selectedItem = orderableGroupService + .findByLotInOrderableGroup(vm.selectedOrderableGroup, vm.selectedLot); + } + + validateDate(); + validateLotCode(selectedItem); + var noErrors = !vm.newLot.expirationDateInvalid && !vm.newLot.lotCodeInvalid; + if (selectedItem && !vm.addedItems.includes(selectedItem) && noErrors) { + vm.addedItems.push(selectedItem); } - }; + } /** * @ngdoc method * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name validateDate + * + * @description + * Validate if expirationDate is a future date. + */ + function validateDate() { + var currentDate = moment(new Date()).format('YYYY-MM-DD'); + if (vm.newLot.expirationDate && vm.newLot.expirationDate < currentDate) { + vm.newLot.expirationDateInvalid = messageService.get('stockEditLotModal.expirationDateInvalid'); + } + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name validateLotCode + * + * @description + * Validate if on line item list exists the same orderable with the same lot code + * + * @param {Object} selectedItem item to add to form + */ + function validateLotCode(selectedItem) { + if (selectedItem && (Object.values(selectedItems).filter(function(item) { + return (item.isAdded || selectedItem.$isNewItem) && isIdenticalOrderableAndLotCode(item, selectedItem); + }).length > 0 || vm.addedItems && vm.addedItems.filter(function(item) { + return isIdenticalOrderableAndLotCode(item, selectedItem); + }).length > 0)) { + vm.newLot.lotCodeInvalid = messageService.get('stockEditLotModal.lotCodeInvalid'); + } + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name isIdenticalOrderableAndLotCode + * + * @description + * Compare product code and lot code in two objects + * + * @param {Object} item item to compare + * @param {Object} itemToCompare item to compare + */ + function isIdenticalOrderableAndLotCode(item, itemToCompare) { + return itemToCompare.orderable.productCode === item.orderable.productCode + && item.lot && itemToCompare.lot && itemToCompare.lot.lotCode === item.lot.lotCode; + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name newLotCodeChanged + * + * @description + * Hides the error message if exists after changed lot code. + */ + function newLotCodeChanged() { + vm.newLot.lotCodeInvalid = undefined; + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController + * @name expirationDateChanged + * + * @description + * Hides the error message if exists after changed expiration date. + */ + function expirationDateChanged() { + vm.newLot.expirationDateInvalid = undefined; + } + + /** + * @ngdoc method + * @methodOf stock-add-products-modal.controller:AddProductsModalController * @name removeAddedProduct * * @description * Removes an already added product and reset its quantity value. */ - vm.removeAddedProduct = function(item) { + function removeAddedProduct(item) { item.quantity = undefined; item.quantityMissingError = undefined; - vm.addedItems = _.without(vm.addedItems, item); - }; + vm.addedItems.splice(vm.addedItems.indexOf(item), 1); + } /** * @ngdoc method @@ -131,27 +282,27 @@ * @description * Validate if quantity is filled in by user. */ - vm.validate = function(item) { + function validate(item) { if (!item.quantity) { item.quantityInvalid = messageService.get('stockAddProductsModal.required'); } else if (item.quantity > MAX_INTEGER_VALUE) { item.quantityInvalid = messageService.get('stockmanagement.numberTooLarge'); } else { item.quantityInvalid = undefined; } - }; + } /** * @ngdoc method * @methodOf stock-add-products-modal.controller:AddProductsModalController * @name confirm * * @description - * Confirm added products and close modal. Will not close modal if any quanity not filled in. + * Confirm added products and close modal. Will not close modal if any quantity not filled in. */ - vm.confirm = function() { + function confirm() { //some items may not have been validated yet, so validate all here. - _.forEach(vm.addedItems, function(item) { + vm.addedItems.forEach(function(item) { vm.validate(item); }); @@ -161,22 +312,41 @@ return !item.quantityInvalid; }); if (noErrors) { + vm.addedItems.forEach(function(item) { + if (item.$isNewItem) { + selectedItems.push(item); + } + }); modalDeferred.resolve(); } - }; + } - modalDeferred.promise.catch(function() { - _.forEach(vm.addedItems, function(item) { - item.quantity = undefined; - item.quantityInvalid = undefined; - }); - }); - - //this function will initiate product select options - function onInit() { - vm.orderableGroups = orderableGroupService.groupByOrderableId(items); + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name lotChanged + * + * @description + * Allows inputs to add missing lot to be displayed. + */ + function lotChanged() { + vm.canAddNewLot = vm.selectedLot && + vm.selectedLot.lotCode === messageService.get('orderableGroupService.addMissingLot'); + initiateNewLotObject(); } - onInit(); + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name initiateNewLotObject + * + * @description + * Initiates new lot object. + */ + function initiateNewLotObject() { + vm.newLot = { + active: true + }; + } } })(); Index: src/stock-add-products-modal/add-products-modal.controller.spec.js =================================================================== diff -u -N -r5a0769bd03716dd9d38c76e051bb8edf1c9e02f5 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-add-products-modal/add-products-modal.controller.spec.js (.../add-products-modal.controller.spec.js) (revision 5a0769bd03716dd9d38c76e051bb8edf1c9e02f5) +++ src/stock-add-products-modal/add-products-modal.controller.spec.js (.../add-products-modal.controller.spec.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -15,209 +15,351 @@ describe('AddProductsModalController', function() { - var vm, deferred, $rootScope, scope, item1; + var vm, deferred, $q, $rootScope, $controller, OrderableDataBuilder, orderableGroupService, + LotDataBuilder, messageService, scope, item1, item2, lot1, lot2, selectedItems, orderable; beforeEach(function() { module('stock-add-products-modal'); module('referencedata'); - inject(function(_$controller_, _messageService_, _$q_, - _$rootScope_, _orderableGroupService_) { - $rootScope = _$rootScope_; - deferred = _$q_.defer(); + inject(function($injector) { + $rootScope = $injector.get('$rootScope'); + $q = $injector.get('$q'); + $controller = $injector.get('$controller'); + OrderableDataBuilder = $injector.get('OrderableDataBuilder'); + orderableGroupService = $injector.get('orderableGroupService'); + LotDataBuilder = $injector.get('LotDataBuilder'); + messageService = $injector.get('messageService'); + }); - scope = $rootScope.$new(); - spyOn(scope, '$broadcast').andCallThrough(); + deferred = $q.defer(); + scope = $rootScope.$new(); + spyOn(scope, '$broadcast').andCallThrough(); - item1 = { - orderable: { - id: 'O1' - }, - lot: { - id: 'L1' - } - }; + orderable = new OrderableDataBuilder() + .withIdentifiers({ + tradeItem: 'trade-item-id-1' + }) + .build(); + lot1 = new LotDataBuilder().build(); + lot2 = new LotDataBuilder() + .withCode('1234') + .build(); - scope.productForm = jasmine.createSpyObj('productForm', ['$setUntouched', '$setPristine']); + item1 = { + orderable: orderable, + lot: lot1 + }; - vm = _$controller_('AddProductsModalController', { - items: [item1], - hasLot: true, - messageService: _messageService_, - modalDeferred: deferred, - orderableGroupService: _orderableGroupService_, - $scope: scope - }); + item2 = { + isAdded: true, + orderable: orderable, + lot: lot2 + }; + + scope.productForm = jasmine.createSpyObj('productForm', ['$setUntouched', '$setPristine']); + + selectedItems = [item1, item2]; + + vm = $controller('AddProductsModalController', { + availableItems: [item1], + hasLot: true, + messageService: messageService, + modalDeferred: deferred, + orderableGroupService: orderableGroupService, + $scope: scope, + hasPermissionToAddNewLot: true, + selectedItems: selectedItems }); + vm.$onInit(); }); - it('should NOT add if select box is empty', function() { - //given - //do nothing here, to simulate that select box is empty + describe('addOneProduct', function() { - //when - vm.addOneProduct(); + it('should NOT add if select box is empty', function() { + //given + //do nothing here, to simulate that select box is empty - //then - expect(vm.addedItems).toEqual([]); - }); + //when + vm.addOneProduct(); - it('should NOT add twice if selected item already added', function() { - //given - vm.selectedOrderableGroup = [item1]; - vm.selectedLot = item1.lot; + //then + expect(vm.addedItems).toEqual([]); + }); - vm.addedItems = [item1]; - //when - vm.addOneProduct(); + it('should NOT add twice if selected item already added', function() { + //given + vm.selectedOrderableGroup = [item1]; + vm.selectedLot = item1.lot; - //then - //only appear once, not twice - expect(vm.addedItems).toEqual([item1]); - }); + vm.addedItems = [item1]; + //when + vm.addOneProduct(); - it('should add if selected item not added yet', function() { - //given - vm.selectedOrderableGroup = [item1]; - vm.selectedLot = item1.lot; + //then + //only appear once, not twice + expect(vm.addedItems).toEqual([item1]); + }); - vm.addedItems = []; + it('should NOT add if expirationDate is invalid', function() { + item1.lot.expirationDate = '2019-09-09'; + vm.addOneProduct(); - //when - vm.addOneProduct(); + expect(vm.addedItems).toEqual([]); + }); - //then - expect(vm.addedItems).toEqual([item1]); - }); + it('should NOT add if the same lot code has already been added', function() { + vm.selectedOrderableGroup = [item1]; + vm.selectedLot = item1.lot; + vm.selectedLot.lotCode = '1234'; + vm.addedItems = []; - it('should remove added product and reset its quantity value', function() { - //given - var item = { - quantity: 123 - }; - vm.addedItems = [item]; + vm.addOneProduct(); - //when - vm.removeAddedProduct(item); + expect(vm.addedItems).toEqual([]); + }); - //then - expect(item.quantity).not.toBeDefined(); - expect(vm.addedItems).toEqual([]); - }); + it('should add when new lot code no added yet', function() { + vm.selectedOrderableGroup = [item1]; + vm.selectedLot = item1.lot; + vm.selectedLot.lotCode = '2233'; + vm.addedItems = []; + vm.addOneProduct(); - it('should reset all item quantities and error messages when cancel', function() { - //given - var item1 = { - quantity: 123, - quantityInvalid: 'blah' - }; - var item2 = { - quantity: 456 - }; - vm.addedItems = [item1, item2]; + expect(vm.addedItems).toEqual([item1]); + }); - //when - //pretend modal was closed by user - deferred.reject(); - $rootScope.$apply(); + it('should add if selected item not added yet', function() { + //given + vm.selectedOrderableGroup = [item1]; + vm.selectedLot = item1.lot; - //then - expect(item1.quantity).not.toBeDefined(); - expect(item1.quantityInvalid).not.toBeDefined(); + vm.addedItems = []; - expect(item2.quantity).not.toBeDefined(); - }); + //when + vm.addOneProduct(); - it('should assign error message when quantity missing', function() { - //given - var item1 = { - quantity: undefined - }; + //then + expect(vm.addedItems).toEqual([item1]); + }); - //when - vm.validate(item1); + it('should add if missing lot provided', function() { + vm.selectedOrderableGroup = [item1]; + vm.newLot.lotCode = 'NewLot001'; - //then - expect(item1.quantityInvalid).toBeDefined(); + vm.addedItems = []; + + var newLot = { + lotCode: vm.newLot.lotCode, + expirationDate: vm.newLot.expirationDate, + tradeItemId: vm.selectedOrderableGroup[0].orderable.identifiers.tradeItem, + active: true + }; + + vm.addOneProduct(); + + expect(vm.addedItems).toEqual([{ + orderable: vm.selectedOrderableGroup[0].orderable, + lot: newLot, + id: undefined, + displayLotMessage: 'NewLot001', + stockOnHand: 0, + $isNewItem: true + }]); + }); }); - it('should remove error message when quantity filled in', function() { - //given - var item1 = { - quantityInvalid: 'blah' - }; + describe('removeAddedProduct', function() { - //when - item1.quantity = 123; - vm.validate(item1); + it('should remove added product and reset its quantity value', function() { + //given + var item = { + quantity: 123 + }; + vm.addedItems = [item]; - //then - expect(item1.quantityInvalid).not.toBeDefined(); + //when + vm.removeAddedProduct(item); + + //then + expect(item.quantity).not.toBeDefined(); + expect(vm.addedItems).toEqual([]); + }); }); - it('should broadcast form submit when confirming', function() { - vm.confirm(); + describe('modal close', function() { - expect(scope.$broadcast).toHaveBeenCalledWith('openlmis-form-submit'); + it('should reset all item quantities and error messages when cancel', function() { + //given + var item1 = { + quantity: 123, + quantityInvalid: 'blah' + }; + var item2 = { + quantity: 456 + }; + vm.addedItems = [item1, item2]; + + //when + //pretend modal was closed by user + deferred.reject(); + $rootScope.$apply(); + + //then + expect(item1.quantity).not.toBeDefined(); + expect(item1.quantityInvalid).not.toBeDefined(); + + expect(item2.quantity).not.toBeDefined(); + }); }); - it('should confirm add products if all items have quantities', function() { - //given - var item1 = { - quantity: 1 - }; - var item2 = { - quantity: 2 - }; - vm.addedItems = [item1, item2]; + describe('validate', function() { - spyOn(deferred, 'resolve'); + it('should assign error message when quantity missing', function() { + //given + var item1 = { + quantity: undefined + }; - //when - vm.confirm(); + //when + vm.validate(item1); - //then - expect(deferred.resolve).toHaveBeenCalled(); + //then + expect(item1.quantityInvalid).toBeDefined(); + }); + + it('should remove error message when quantity filled in', function() { + //given + var item1 = { + quantityInvalid: 'blah' + }; + + //when + item1.quantity = 123; + vm.validate(item1); + + //then + expect(item1.quantityInvalid).not.toBeDefined(); + }); }); - it('should NOT confirm add products if some items have no quantity', function() { - //given - var item1 = { - quantity: 1 - }; - var item2 = { - quantity: undefined - }; - vm.addedItems = [item1, item2]; + describe('confirm', function() { - spyOn(deferred, 'resolve'); + it('should broadcast form submit when confirming', function() { + vm.confirm(); - //when - vm.confirm(); + expect(scope.$broadcast).toHaveBeenCalledWith('openlmis-form-submit'); + }); - //then - expect(deferred.resolve).not.toHaveBeenCalled(); + it('should confirm add products if all items have quantities', function() { + //given + var item1 = { + quantity: 1 + }; + var item2 = { + quantity: 2 + }; + vm.addedItems = [item1, item2]; + + spyOn(deferred, 'resolve'); + + //when + vm.confirm(); + + //then + expect(deferred.resolve).toHaveBeenCalled(); + }); + + it('should NOT confirm add products if some items have no quantity', function() { + //given + var item1 = { + quantity: 1 + }; + var item2 = { + quantity: undefined + }; + vm.addedItems = [item1, item2]; + + spyOn(deferred, 'resolve'); + + //when + vm.confirm(); + + //then + expect(deferred.resolve).not.toHaveBeenCalled(); + }); }); describe('orderableSelectionChanged', function() { it('should unselect lot', function() { - vm.selectedLot = vm.items[0].lot; + vm.selectedLot = vm.availableItems[0].lot; vm.orderableSelectionChanged(); expect(vm.selectedLot).toBe(null); }); + it('should clear new lot code', function() { + vm.newLot.lotCode = 'NewLot001'; + vm.orderableSelectionChanged(); + + expect(vm.newLot.lotCode).not.toBeDefined(null); + }); + + it('should clear new lot expiration date', function() { + vm.newLot.expirationDate = '2019-08-06'; + vm.orderableSelectionChanged(); + + expect(vm.newLot.expirationDate).not.toBeDefined(); + }); + + it('should set canAddNewLot as false', function() { + vm.canAddNewLot = true; + vm.orderableSelectionChanged(); + + expect(vm.canAddNewLot).toBeFalsy(); + }); + it('should clear form', function() { - vm.selectedLot = vm.items[0].lot; + vm.selectedLot = vm.availableItems[0].lot; vm.orderableSelectionChanged(); expect(scope.productForm.$setPristine).toHaveBeenCalled(); expect(scope.productForm.$setUntouched).toHaveBeenCalled(); }); - }); + describe('lotChanged', function() { + + it('should clear new lot code', function() { + vm.newLot.lotCode = 'NewLot001'; + vm.lotChanged(); + + expect(vm.newLot.lotCode).not.toBeDefined(); + }); + + it('should clear new lot expiration date', function() { + vm.newLot.expirationDate = '2019-08-06'; + vm.lotChanged(); + + expect(vm.newLot.expirationDate).not.toBeDefined(); + }); + + it('should set canAddNewLot as true', function() { + vm.selectedLot = vm.availableItems[0].lot; + vm.selectedLot.lotCode = 'orderableGroupService.addMissingLot'; + vm.lotChanged(); + + expect(vm.canAddNewLot).toBeTruthy(); + }); + + it('should set canAddNewLot as false', function() { + vm.selectedLot = vm.availableItems[0].lot; + vm.lotChanged(); + + expect(vm.canAddNewLot).toBeFalsy(); + }); + }); }); \ No newline at end of file Index: src/stock-add-products-modal/add-products-modal.html =================================================================== diff -u -N -r2ac0b2f292fc3dbdf76ee80d46ac804cedfff86a -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-add-products-modal/add-products-modal.html (.../add-products-modal.html) (revision 2ac0b2f292fc3dbdf76ee80d46ac804cedfff86a) +++ src/stock-add-products-modal/add-products-modal.html (.../add-products-modal.html) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -12,23 +12,36 @@ \ No newline at end of file Index: src/stock-add-products-modal/add-products-modal.service.js =================================================================== diff -u -N -r71a56909f72a67c2658497b0de966d117794f9b5 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-add-products-modal/add-products-modal.service.js (.../add-products-modal.service.js) (revision 71a56909f72a67c2658497b0de966d117794f9b5) +++ src/stock-add-products-modal/add-products-modal.service.js (.../add-products-modal.service.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -41,21 +41,38 @@ * @description * Shows modal that allows users to choose products. * - * @return {Promise} resolved with selected products. + * @param {Array} availableItems orderable + lot items that can be selected + * @param {Array} selectedItems orderable + lot items that were added already + * @return {Promise} resolved with selected products. */ - function show(items, hasLot) { + function show(availableItems, selectedItems) { return openlmisModalService.createDialog( { controller: 'AddProductsModalController', controllerAs: 'vm', templateUrl: 'stock-add-products-modal/add-products-modal.html', show: true, resolve: { - items: function() { - return items; + availableItems: function() { + return availableItems; }, - hasLot: function() { - return hasLot; + selectedItems: function() { + return selectedItems; + }, + hasPermissionToAddNewLot: function(permissionService, ADMINISTRATION_RIGHTS, + authorizationService) { + return permissionService.hasPermissionWithAnyProgramAndAnyFacility( + authorizationService.getUser().user_id, + { + right: ADMINISTRATION_RIGHTS.LOTS_MANAGE + } + ) + .then(function() { + return true; + }) + .catch(function() { + return false; + }); } } } Index: src/stock-add-products-modal/add-products-modal.service.spec.js =================================================================== diff -u -N --- src/stock-add-products-modal/add-products-modal.service.spec.js (revision 0) +++ src/stock-add-products-modal/add-products-modal.service.spec.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,78 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('addProductsModalService', function() { + + var that = this; + + beforeEach(function() { + + module('stock-add-products-modal'); + + inject(function($injector) { + that.openlmisModalService = $injector.get('openlmisModalService'); + that.addProductsModalService = $injector.get('addProductsModalService'); + that.LotDataBuilder = $injector.get('LotDataBuilder'); + that.OrderableDataBuilder = $injector.get('OrderableDataBuilder'); + that.$q = $injector.get('$q'); + }); + + that.promise = that.$q.defer(); + spyOn(that.openlmisModalService, 'createDialog').andCallFake(function(config) { + that.config = config; + return that.promise; + }); + + that.items = [ + { + orderable: new that.OrderableDataBuilder().build(), + lot: new that.LotDataBuilder().build() + }, + { + orderable: new that.OrderableDataBuilder().build(), + lot: new that.LotDataBuilder().build() + } + ]; + + that.lineItems = [ + { + orderable: new that.OrderableDataBuilder().build(), + lot: new that.LotDataBuilder().build() + }, + { + orderable: new that.OrderableDataBuilder().build(), + lot: new that.LotDataBuilder().build() + } + ]; + }); + + describe('show', function() { + + it('should call createDialog function', function() { + + that.addProductsModalService.show(that.items, that.lineItems); + + expect(that.openlmisModalService.createDialog).toHaveBeenCalled(); + + expect(that.config.controller).toBe('AddProductsModalController'); + expect(that.config.controllerAs).toBe('vm'); + expect(that.config.templateUrl).toBe('stock-add-products-modal/add-products-modal.html'); + expect(that.config.show).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.availableItems)).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.selectedItems)).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.hasPermissionToAddNewLot)).toBeTruthy(); + }); + }); +}); \ No newline at end of file Index: src/stock-add-products-modal/messages_en.json =================================================================== diff -u -N -r68e12a92dc8bdea23fa96bfa636837d904c442b6 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-add-products-modal/messages_en.json (.../messages_en.json) (revision 68e12a92dc8bdea23fa96bfa636837d904c442b6) +++ src/stock-add-products-modal/messages_en.json (.../messages_en.json) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -3,6 +3,8 @@ "stockAddProductsModal.productCode": "Product Code", "stockAddProductsModal.lotCode": "Lot Code", "stockAddProductsModal.expiryDate": "Expiry Date", + "stockAddProductsModal.newLotCode": "New Batch Number", + "stockAddProductsModal.newLotExpirationDate": "Expiry Date", "stockAddProductsModal.addProduct": "Add Product", "stockAddProductsModal.product": "Product", "stockAddProductsModal.currentStock": "Current Stock", Index: src/stock-adjustment-creation/adjustment-creation.controller.js =================================================================== diff -u -N -r6f6b2e0120fc1fd38bdacee53ee04c3c57a100af -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-adjustment-creation/adjustment-creation.controller.js (.../adjustment-creation.controller.js) (revision 6f6b2e0120fc1fd38bdacee53ee04c3c57a100af) +++ src/stock-adjustment-creation/adjustment-creation.controller.js (.../adjustment-creation.controller.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -33,18 +33,26 @@ 'orderableGroups', 'reasons', 'confirmService', 'messageService', 'user', 'adjustmentType', 'srcDstAssignments', 'stockAdjustmentCreationService', 'notificationService', 'offlineService', 'orderableGroupService', 'MAX_INTEGER_VALUE', 'VVM_STATUS', 'loadingModalService', 'alertService', - 'dateUtils', 'displayItems', 'ADJUSTMENT_TYPE', 'UNPACK_REASONS', 'REASON_TYPES', 'STOCKCARD_STATUS' + 'dateUtils', 'displayItems', 'ADJUSTMENT_TYPE', 'UNPACK_REASONS', 'REASON_TYPES', 'STOCKCARD_STATUS', + 'hasPermissionToAddNewLot', 'LotResource', '$q', 'editLotModalService', 'moment' ]; function controller($scope, $state, $stateParams, $filter, confirmDiscardService, program, facility, orderableGroups, reasons, confirmService, messageService, user, adjustmentType, srcDstAssignments, stockAdjustmentCreationService, notificationService, offlineService, orderableGroupService, MAX_INTEGER_VALUE, VVM_STATUS, loadingModalService, alertService, dateUtils, displayItems, ADJUSTMENT_TYPE, UNPACK_REASONS, REASON_TYPES, - STOCKCARD_STATUS) { + STOCKCARD_STATUS, hasPermissionToAddNewLot, LotResource, $q, editLotModalService, moment) { var vm = this, previousAdded = {}; + vm.expirationDateChanged = expirationDateChanged; + vm.newLotCodeChanged = newLotCodeChanged; + vm.validateExpirationDate = validateExpirationDate; + vm.lotChanged = lotChanged; + vm.addProduct = addProduct; + vm.hasPermissionToAddNewLot = hasPermissionToAddNewLot; + /** * @ngdoc property * @propertyOf stock-adjustment-creation.controller:StockAdjustmentCreationController @@ -91,6 +99,17 @@ }; /** + * @ngdoc property + * @propertyOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name newLot + * @type {Object} + * + * @description + * Holds new lot object. + */ + vm.newLot = undefined; + + /** * @ngdoc method * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController * @name search @@ -120,21 +139,43 @@ * @description * Add a product for stock adjustment. */ - vm.addProduct = function() { - var selectedItem = orderableGroupService - .findByLotInOrderableGroup(vm.selectedOrderableGroup, vm.selectedLot); + function addProduct() { + var selectedItem; - vm.addedLineItems.unshift(_.extend({ - $errors: {}, - $previewSOH: selectedItem.stockOnHand - }, - selectedItem, copyDefaultValue())); + if (vm.selectedOrderableGroup && vm.selectedOrderableGroup.length) { + vm.newLot.tradeItemId = vm.selectedOrderableGroup[0].orderable.identifiers.tradeItem; + } - previousAdded = vm.addedLineItems[0]; + if (vm.newLot.lotCode) { + var createdLot = angular.copy(vm.newLot); + selectedItem = orderableGroupService + .findByLotInOrderableGroup(vm.selectedOrderableGroup, createdLot, true); + selectedItem.$isNewItem = true; + } else { + selectedItem = orderableGroupService + .findByLotInOrderableGroup(vm.selectedOrderableGroup, vm.selectedLot); + } - vm.search(); - }; + vm.newLot.expirationDateInvalid = undefined; + vm.newLot.lotCodeInvalid = undefined; + validateExpirationDate(); + validateLotCode(vm.addedLineItems, selectedItem); + validateLotCode(vm.allItems, selectedItem); + var noErrors = !vm.newLot.expirationDateInvalid && !vm.newLot.lotCodeInvalid; + if (noErrors) { + vm.addedLineItems.unshift(_.extend({ + $errors: {}, + $previewSOH: selectedItem.stockOnHand + }, + selectedItem, copyDefaultValue())); + + previousAdded = vm.addedLineItems[0]; + + vm.search(); + } + } + function copyDefaultValue() { var defaultDate; if (previousAdded.occurredDate) { @@ -251,6 +292,20 @@ /** * @ngdoc method * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name lotChanged + * + * @description + * Allows inputs to add missing lot to be displayed. + */ + function lotChanged() { + vm.canAddNewLot = vm.selectedLot + && vm.selectedLot.lotCode === messageService.get('orderableGroupService.addMissingLot'); + initiateNewLotObject(); + } + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController * @name validateDate * * @description @@ -313,13 +368,16 @@ //reset selected lot, so that lot field has no default value vm.selectedLot = null; + initiateNewLotObject(); + vm.canAddNewLot = false; + //same as above $scope.productForm.$setUntouched(); //make form good as new, so errors won't persist $scope.productForm.$setPristine(); - vm.lots = orderableGroupService.lotsOf(vm.selectedOrderableGroup); + vm.lots = orderableGroupService.lotsOf(vm.selectedOrderableGroup, vm.hasPermissionToAddNewLot); vm.selectedOrderableHasLots = vm.lots.length > 0; }; @@ -389,24 +447,102 @@ generateKitConstituentLineItem(addedLineItems); - stockAdjustmentCreationService.submitAdjustments(program.id, facility.id, addedLineItems, adjustmentType) - .then(function() { - if (offlineService.isOffline()) { - notificationService.offline(vm.key('submittedOffline')); - } else { - notificationService.success(vm.key('submitted')); + var lotPromises = [], + errorLots = []; + var distinctLots = []; + var lotResource = new LotResource(); + addedLineItems.forEach(function(lineItem) { + if (lineItem.lot && lineItem.$isNewItem && _.isUndefined(lineItem.lot.id) && + !listContainsTheSameLot(distinctLots, lineItem.lot)) { + distinctLots.push(lineItem.lot); + } + }); + distinctLots.forEach(function(lot) { + lotPromises.push(lotResource.create(lot) + .then(function(createResponse) { + vm.addedLineItems.forEach(function(item) { + if (item.lot.lotCode === lot.lotCode) { + item.$isNewItem = false; + addItemToOrderableGroups(item); + } + }); + return createResponse; + }) + .catch(function(response) { + if (response.data.messageKey === + 'referenceData.error.lot.lotCode.mustBeUnique') { + errorLots.push(lot.lotCode); + } + })); + }); + + return $q.all(lotPromises) + .then(function(responses) { + if (errorLots !== undefined && errorLots.length > 0) { + return $q.reject(); } - $state.go('openlmis.stockmanagement.stockCardSummaries', { - facility: facility.id, - program: program.id, - active: STOCKCARD_STATUS.ACTIVE + responses.forEach(function(lot) { + addedLineItems.forEach(function(lineItem) { + if (lineItem.lot && lineItem.lot.lotCode === lot.lotCode + && lineItem.lot.tradeItemId === lot.tradeItemId) { + lineItem.lot = lot; + } + }); + return addedLineItems; }); - }, function(errorResponse) { + + stockAdjustmentCreationService.submitAdjustments( + program.id, facility.id, addedLineItems, adjustmentType + ) + .then(function() { + if (offlineService.isOffline()) { + notificationService.offline(vm.key('submittedOffline')); + } else { + notificationService.success(vm.key('submitted')); + } + $state.go('openlmis.stockmanagement.stockCardSummaries', { + facility: facility.id, + program: program.id, + active: STOCKCARD_STATUS.ACTIVE + }); + }, function(errorResponse) { + loadingModalService.close(); + alertService.error(errorResponse.data.message); + }); + }) + .catch(function(errorResponse) { loadingModalService.close(); + if (errorLots) { + alertService.error('stockPhysicalInventoryDraft.lotCodeMustBeUnique', + errorLots.join(', ')); + vm.selectedOrderableGroup = undefined; + vm.selectedLot = undefined; + vm.lotChanged(); + return $q.reject(errorResponse.data.message); + } alertService.error(errorResponse.data.message); }); } + function addItemToOrderableGroups(item) { + vm.orderableGroups.forEach(function(array) { + if (array[0].orderable.id === item.orderable.id) { + array.push(angular.copy(item)); + } + }); + } + + function listContainsTheSameLot(list, lot) { + var itemExistsOnList = false; + list.forEach(function(item) { + if (item.lotCode === lot.lotCode && + item.tradeItemId === lot.tradeItemId) { + itemExistsOnList = true; + } + }); + return itemExistsOnList; + } + function generateKitConstituentLineItem(addedLineItems) { if (adjustmentType.state !== ADJUSTMENT_TYPE.KIT_UNPACK.state) { return; @@ -432,6 +568,9 @@ } function onInit() { + var copiedOrderableGroups = angular.copy(orderableGroups); + vm.allItems = _.flatten(copiedOrderableGroups); + $state.current.label = messageService.get(vm.key('title'), { facilityCode: facility.code, facilityName: facility.name, @@ -481,10 +620,20 @@ vm.orderableGroups = orderableGroups; vm.hasLot = false; vm.orderableGroups.forEach(function(group) { - vm.hasLot = vm.hasLot || orderableGroupService.lotsOf(group).length > 0; + vm.hasLot = vm.hasLot || orderableGroupService.lotsOf(group, hasPermissionToAddNewLot).length > 0; }); vm.showVVMStatusColumn = orderableGroupService.areOrderablesUseVvm(vm.orderableGroups); + vm.hasPermissionToAddNewLot = hasPermissionToAddNewLot; + vm.canAddNewLot = false; + initiateNewLotObject(); } + + function initiateNewLotObject() { + vm.newLot = { + active: true + }; + } + function initStateParams() { $stateParams.page = getPageNumber(); $stateParams.program = program; @@ -503,6 +652,109 @@ return pageNumber; } + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name editLot + * + * @description + * Pops up a modal for users to edit lot for selected line item. + * + * @param {Object} lineItem line items to be edited. + */ + vm.editLot = function(lineItem) { + var oldLotCode = lineItem.lot.lotCode; + var oldLotExpirationDate = lineItem.lot.expirationDate; + editLotModalService.show(lineItem, vm.allItems, vm.addedLineItems).then(function() { + $stateParams.displayItems = vm.displayItems; + if (oldLotCode === lineItem.lot.lotCode + && oldLotExpirationDate !== lineItem.lot.expirationDate) { + vm.addedLineItems.forEach(function(item) { + if (item.lot && item.lot.lotCode === oldLotCode && + oldLotExpirationDate === item.lot.expirationDate) { + item.lot.expirationDate = lineItem.lot.expirationDate; + } + }); + } + }); + }; + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name canEditLot + * + * @description + * Checks if user can edit lot. + * + * @param {Object} lineItem line item to edit + */ + vm.canEditLot = function(lineItem) { + return vm.hasPermissionToAddNewLot && lineItem.lot && lineItem.$isNewItem; + }; + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name validateExpirationDate + * + * @description + * Validate if expirationDate is a future date. + */ + function validateExpirationDate() { + var currentDate = moment(new Date()).format('YYYY-MM-DD'); + + if (vm.newLot.expirationDate && vm.newLot.expirationDate < currentDate) { + vm.newLot.expirationDateInvalid = messageService.get('stockEditLotModal.expirationDateInvalid'); + } + } + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name expirationDateChanged + * + * @description + * Hides the error message if exists after changed expiration date. + */ + function expirationDateChanged() { + vm.newLot.expirationDateInvalid = undefined; + } + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name newLotCodeChanged + * + * @description + * Hides the error message if exists after changed new lot code. + */ + function newLotCodeChanged() { + vm.newLot.lotCodeInvalid = undefined; + } + + /** + * @ngdoc method + * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController + * @name validateLotCode + * + * @description + * Validate if on line item list exists the same orderable with the same lot code + */ + function validateLotCode(listItems, selectedItem) { + if (selectedItem && selectedItem.$isNewItem) { + listItems.forEach(function(lineItem) { + if (lineItem.orderable && lineItem.lot && selectedItem.lot && + lineItem.orderable.productCode === selectedItem.orderable.productCode && + selectedItem.lot.lotCode === lineItem.lot.lotCode && + ((!lineItem.$isNewItem) || (lineItem.$isNewItem && + selectedItem.lot.expirationDate !== lineItem.lot.expirationDate))) { + vm.newLot.lotCodeInvalid = messageService.get('stockEditLotModal.lotCodeInvalid'); + } + }); + } + } + onInit(); } })(); Index: src/stock-adjustment-creation/adjustment-creation.controller.spec.js =================================================================== diff -u -N -r6f6b2e0120fc1fd38bdacee53ee04c3c57a100af -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-adjustment-creation/adjustment-creation.controller.spec.js (.../adjustment-creation.controller.spec.js) (revision 6f6b2e0120fc1fd38bdacee53ee04c3c57a100af) +++ src/stock-adjustment-creation/adjustment-creation.controller.spec.js (.../adjustment-creation.controller.spec.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -18,7 +18,7 @@ var vm, q, rootScope, state, stateParams, facility, program, confirmService, VVM_STATUS, messageService, scope, stockAdjustmentCreationService, reasons, $controller, ADJUSTMENT_TYPE, ProgramDataBuilder, FacilityDataBuilder, ReasonDataBuilder, OrderableGroupDataBuilder, OrderableDataBuilder, alertService, notificationService, - orderableGroups, LotDataBuilder, UNPACK_REASONS; + orderableGroups, LotDataBuilder, UNPACK_REASONS, LotResource; beforeEach(function() { @@ -32,7 +32,7 @@ }); }); - inject(function($q, $rootScope, $injector) { + inject(function($injector) { q = $injector.get('$q'); rootScope = $injector.get('$rootScope'); stateParams = $injector.get('$stateParams'); @@ -51,9 +51,12 @@ notificationService = $injector.get('notificationService'); LotDataBuilder = $injector.get('LotDataBuilder'); UNPACK_REASONS = $injector.get('UNPACK_REASONS'); + LotResource = $injector.get('LotResource'); this.OrderableDataBuilder = $injector.get('OrderableDataBuilder'); this.OrderableChildrenDataBuilder = $injector.get('OrderableChildrenDataBuilder'); this.offlineService = $injector.get('offlineService'); + this.editLotModalService = $injector.get('editLotModalService'); + spyOn(this.editLotModalService, 'show'); state = jasmine.createSpyObj('$state', ['go']); state.current = { @@ -420,6 +423,22 @@ expect(notificationService.success).not.toHaveBeenCalled(); }); + it('should not submit if new lot code exists in the database', function() { + spyOn(LotResource.prototype, 'query').andCallFake(function(response) { + response.numberOfElements = 1; + return q.resolve(response); + }); + vm.submit(); + rootScope.$apply(); + + expect(state.go).toHaveBeenCalledWith(state.current.name, stateParams, { + reload: false, + notify: false + }); + + expect(notificationService.success).not.toHaveBeenCalled(); + }); + it('should generate kit constituent if the state is unpacking', function() { spyOn(stockAdjustmentCreationService, 'submitAdjustments'); stockAdjustmentCreationService.submitAdjustments.andReturn(q.resolve()); @@ -492,6 +511,39 @@ }); + describe('lotChanged', function() { + + it('should clear new lot code', function() { + vm.newLot.lotCode = 'NewLot001'; + vm.lotChanged(); + + expect(vm.newLot.lotCode).not.toBeDefined(); + }); + + it('should clear new lot expiration date', function() { + vm.newLot.expirationDate = '2019-08-06'; + vm.lotChanged(); + + expect(vm.newLot.expirationDate).not.toBeDefined(); + }); + + it('should set canAddNewLot as true', function() { + vm.selectedLot = new LotDataBuilder() + .withCode('orderableGroupService.addMissingLot') + .build(); + vm.lotChanged(); + + expect(vm.canAddNewLot).toBeTruthy(); + }); + + it('should set canAddNewLot as false', function() { + vm.selectedLot = new LotDataBuilder().build(); + vm.lotChanged(); + + expect(vm.canAddNewLot).toBeFalsy(); + }); + }); + function initController(orderableGroups, adjustmentType) { return $controller('StockAdjustmentCreationController', { $scope: scope, @@ -504,7 +556,9 @@ user: {}, reasons: reasons, orderableGroups: orderableGroups, - displayItems: [] + displayItems: [], + hasPermissionToAddNewLot: true, + editLotModalService: this.editLotModalService }); } Index: src/stock-adjustment-creation/adjustment-creation.html =================================================================== diff -u -N -r2deec9f58c4e3fb36bfaa34a018f33f723a41913 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-adjustment-creation/adjustment-creation.html (.../adjustment-creation.html) (revision 2deec9f58c4e3fb36bfaa34a018f33f723a41913) +++ src/stock-adjustment-creation/adjustment-creation.html (.../adjustment-creation.html) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -25,9 +25,22 @@
+
+ + + + +
@@ -54,7 +67,14 @@ {{lineItem.orderable.productCode}} {{lineItem.orderable | productName}} - {{lineItem.displayLotMessage}} + + + + + {{lineItem.displayLotMessage}} + {{lineItem.lot.expirationDate | openlmisDate}} {{lineItem.$previewSOH}} Index: src/stock-adjustment-creation/adjustment-creation.module.js =================================================================== diff -u -N -r90e41f6fd84b15d6c713202bc4bb0f30e58edccf -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-adjustment-creation/adjustment-creation.module.js (.../adjustment-creation.module.js) (revision 90e41f6fd84b15d6c713202bc4bb0f30e58edccf) +++ src/stock-adjustment-creation/adjustment-creation.module.js (.../adjustment-creation.module.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -26,7 +26,9 @@ 'stock-valid-reason', 'referencedata-program', 'referencedata-facility', + 'referencedata-lot', 'stock-unpack-kit', - 'stock-reasons-modal' + 'stock-reasons-modal', + 'stock-edit-lot-modal' ]); })(); Index: src/stock-adjustment-creation/adjustment-creation.routes.js =================================================================== diff -u -N -r89e13f9bb25d1aba1bce0602bcffa9aef6f2cdbd -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-adjustment-creation/adjustment-creation.routes.js (.../adjustment-creation.routes.js) (revision 89e13f9bb25d1aba1bce0602bcffa9aef6f2cdbd) +++ src/stock-adjustment-creation/adjustment-creation.routes.js (.../adjustment-creation.routes.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -81,6 +81,9 @@ }, srcDstAssignments: function() { return undefined; + }, + hasPermissionToAddNewLot: function() { + return false; } } }); Index: src/stock-edit-lot-modal/edit-lot-modal.controller.js =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.controller.js (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.controller.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,194 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc controller + * @name stock-edit-lot-modal.controller:EditLotModalController + * + * @description + * Controller for managing stock lot edit. + */ + angular + .module('stock-edit-lot-modal') + .controller('EditLotModalController', controller); + + controller.$inject = ['selectedItem', 'modalDeferred', 'messageService', 'moment', 'allLineItems', + 'addedLineItems']; + + function controller(selectedItem, modalDeferred, messageService, moment, allLineItems, addedLineItems) { + + var vm = this; + vm.$onInit = onInit; + vm.updateItem = updateItem; + vm.expirationDateChanged = expirationDateChanged; + vm.lotCodeChanged = lotCodeChanged; + + /** + * @ngdoc property + * @propertyOf stock-edit-lot-modal.controller:EditLotModalController + * @name selectedItem + * @type {Object} + * + * @description + * Selected item on form. + */ + vm.selectedItem = undefined; + + /** + * @ngdoc property + * @propertyOf stock-edit-lot-modal.controller:EditLotModalController + * @name allLineItems + * @type {Array} + * + * @description + * All line items + */ + vm.allLineItems = undefined; + + /** + * @ngdoc property + * @propertyOf stock-edit-lot-modal.controller:EditLotModalController + * @name addedLineItems + * @type {Array} + * + * @description + * Line items added to form + */ + vm.addedLineItems = undefined; + + /** + * @ngdoc property + * @propertyOf stock-edit-lot-modal.controller:EditLotModalController + * @name newLot + * @type {Object} + * + * @description + * Holds new lot object. + */ + vm.newLot = undefined; + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name $onInit + * + * @description + * Initialization method of the EditLotModalController. + */ + function onInit() { + vm.selectedItem = angular.copy(selectedItem); + vm.allLineItems = allLineItems; + vm.addedLineItems = addedLineItems; + vm.newLot = vm.selectedItem.lot; + } + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name updateItem + * + * @description + * Update lot of item on form if there are no errors. + */ + function updateItem() { + vm.newLot.expirationDateInvalid = undefined; + vm.newLot.lotCodeInvalid = undefined; + + validateDate(); + validateLotCode(); + + var noErrors = !vm.newLot.expirationDateInvalid && !vm.newLot.lotCodeInvalid; + + if (noErrors) { + selectedItem.lot = vm.newLot; + selectedItem.displayLotMessage = vm.newLot.lotCode; + modalDeferred.resolve(); + } + } + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name validateDate + * + * @description + * Validate if expirationDate is a future date. + */ + function validateDate() { + var currentDate = moment(new Date()).format('YYYY-MM-DD'); + + if (vm.newLot.expirationDate && vm.newLot.expirationDate < currentDate) { + vm.newLot.expirationDateInvalid = messageService.get('stockEditLotModal.expirationDateInvalid'); + } else if (vm.addedLineItems) { + vm.addedLineItems.forEach(function(lineItem) { + if (lineItem.$isNewItem && lineItem.orderable.productCode === vm.selectedItem.orderable.productCode + && lineItem.lot && vm.selectedItem.lot && + vm.selectedItem.lot.lotCode === lineItem.lot.lotCode + && vm.selectedItem.lot.expirationDate !== lineItem.lot.expirationDate + && (vm.selectedItem.lot.lotCode !== selectedItem.lot.lotCode)) { + vm.newLot.expirationDateInvalid = + messageService.get('stockEditLotModal.expirationDateInvalidForLotCode'); + } + }); + } + } + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name validateLotCode + * + * @description + * Validate if on line item list exists the same orderable with the same lot code + */ + function validateLotCode() { + vm.allLineItems.forEach(function(lineItem) { + if (lineItem.orderable.productCode === vm.selectedItem.orderable.productCode + && lineItem.lot && vm.selectedItem.lot && vm.selectedItem.lot.lotCode === lineItem.lot.lotCode + && !(vm.selectedItem.lot.lotCode === selectedItem.lot.lotCode)) { + vm.newLot.lotCodeInvalid = messageService.get('stockEditLotModal.lotCodeInvalid'); + } + }); + } + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name expirationDateChanged + * + * @description + * Hides the error message if exists after changed expiration date. + */ + function expirationDateChanged() { + vm.newLot.expirationDateInvalid = undefined; + } + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.controller:EditLotModalController + * @name lotCodeChanged + * + * @description + * Hides the error message if exists after changed lot code. + */ + function lotCodeChanged() { + vm.newLot.lotCodeInvalid = undefined; + } + } +})(); \ No newline at end of file Index: src/stock-edit-lot-modal/edit-lot-modal.controller.spec.js =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.controller.spec.js (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.controller.spec.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,126 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('EditLotModalController', function() { + + var that = this; + + beforeEach(function() { + + module('stock-edit-lot-modal'); + + inject(function($injector) { + that.$controller = $injector.get('$controller'); + that.$rootScope = $injector.get('$rootScope'); + that.$q = $injector.get('$q'); + that.LotDataBuilder = $injector.get('LotDataBuilder'); + that.OrderableDataBuilder = $injector.get('OrderableDataBuilder'); + }); + + that.deferred = that.$q.defer(); + + that.orderable = new that.OrderableDataBuilder() + .withIdentifiers({ + tradeItem: 'trade-item-id-1' + }) + .build(); + that.lot1 = new that.LotDataBuilder().build(); + that.lot2 = new that.LotDataBuilder() + .withCode('1234') + .build(); + + that.selectedItem = { + isAdded: true, + orderable: that.orderable, + lot: that.lot1 + }; + + that.item = { + isAdded: true, + orderable: that.orderable, + lot: that.lot2 + }; + + that.vm = that.$controller('EditLotModalController', { + allLineItems: [that.item, that.selectedItem], + addedLineItems: [that.item], + selectedItem: that.selectedItem, + newLot: that.selectedItem.lot, + modalDeferred: that.deferred + }); + that.vm.$onInit(); + }); + + describe('modal close', function() { + + it('should not assign values from newLot to selectedItem', function() { + that.vm.newLot.lotCode = 'test123'; + + that.deferred.reject(); + that.$rootScope.$apply(); + + expect(that.selectedItem.lot.lotCode).not.toEqual(that.vm.newLot.lotCode); + }); + }); + + describe('updateItem', function() { + + it('should assign values from newLot to selectedItem', function() { + that.vm.newLot.lotCode = 'test123'; + that.vm.newLot.expirationDate = new Date(); + + spyOn(that.deferred, 'resolve'); + that.vm.updateItem(); + + expect(that.selectedItem.lot.lotCode).toEqual(that.vm.newLot.lotCode); + expect(that.selectedItem.lot.expirationDate).toEqual(that.vm.newLot.expirationDate); + }); + }); + + describe('validate', function() { + + it('should assign error message when expirationDate smaller than current date', function() { + that.vm.newLot.expirationDate = '2019-09-09'; + + that.vm.updateItem(); + + expect(that.vm.newLot.expirationDateInvalid).toBeDefined(); + }); + + it('should not assign error message when expirationDate equals current date', function() { + that.vm.newLot.expirationDate = new Date(); + + that.vm.updateItem(); + + expect(that.vm.newLot.expirationDateInvalid).not.toBeDefined(); + }); + + it('should assign error message when new lot code exist in allLineItems', function() { + that.vm.newLot.lotCode = '1234'; + + that.vm.updateItem(); + + expect(that.vm.newLot.lotCodeInvalid).toBeDefined(); + }); + + it('should not assign error message when new lot code not exist in allLineItems', function() { + that.vm.newLot.lotCode = '2233'; + + that.vm.updateItem(); + + expect(that.vm.newLot.lotCodeInvalid).not.toBeDefined(); + }); + }); +}); \ No newline at end of file Index: src/stock-edit-lot-modal/edit-lot-modal.html =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.html (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.html (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,36 @@ + \ No newline at end of file Index: src/stock-edit-lot-modal/edit-lot-modal.module.js =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.module.js (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.module.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,33 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @module stock-edit-lot-modal + * + * @description + * Provides modal to editing lot. + */ + angular.module('stock-edit-lot-modal', [ + 'openlmis-modal', + 'stockmanagement', + 'referencedata-lot', + 'referencedata-orderable' + ]); + +})(); \ No newline at end of file Index: src/stock-edit-lot-modal/edit-lot-modal.service.js =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.service.js (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.service.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,74 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name stock-edit-lot-modal.editLotModalService + * + * @description + * This service will pop up a modal window for user to edit lot. + */ + angular + .module('stock-edit-lot-modal') + .service('editLotModalService', service); + + service.$inject = ['openlmisModalService']; + + function service(openlmisModalService) { + + this.show = show; + + /** + * @ngdoc method + * @methodOf stock-edit-lot-modal.editLotModalService + * @name show + * + * @description + * Shows modal that allows users to edit lot. + * + * @param {Object} selectedItem item that was selected on form + * @param {Array} addedLineItems line items added to form + * @return {Promise} resolved with edited lot + */ + function show(selectedItem, allLineItems, addedLineItems) { + return openlmisModalService.createDialog( + { + controller: 'EditLotModalController', + controllerAs: 'vm', + templateUrl: 'stock-edit-lot-modal/edit-lot-modal.html', + show: true, + resolve: { + selectedItem: function() { + return selectedItem; + }, + allLineItems: function() { + return allLineItems; + }, + addedLineItems: function() { + return addedLineItems; + } + } + } + ).promise.finally(function() { + angular.element('.popover').popover('destroy'); + }); + } + } + +})(); \ No newline at end of file Index: src/stock-edit-lot-modal/edit-lot-modal.service.spec.js =================================================================== diff -u -N --- src/stock-edit-lot-modal/edit-lot-modal.service.spec.js (revision 0) +++ src/stock-edit-lot-modal/edit-lot-modal.service.spec.js (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,63 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('editLotModalService', function() { + + var that = this; + + beforeEach(function() { + + module('stock-edit-lot-modal'); + + inject(function($injector) { + that.openlmisModalService = $injector.get('openlmisModalService'); + that.editLotModalService = $injector.get('editLotModalService'); + that.LotDataBuilder = $injector.get('LotDataBuilder'); + that.OrderableDataBuilder = $injector.get('OrderableDataBuilder'); + that.$q = $injector.get('$q'); + }); + + that.promise = that.$q.defer(); + spyOn(that.openlmisModalService, 'createDialog').andCallFake(function(config) { + that.config = config; + return that.promise; + }); + + that.selectedItem = + { + orderable: new that.OrderableDataBuilder().build(), + lot: new that.LotDataBuilder().build() + }; + + }); + + describe('show', function() { + + it('should call createDialog function', function() { + + that.editLotModalService.show(that.selectedItem, that.allLineItems, that.addedLineItems); + + expect(that.openlmisModalService.createDialog).toHaveBeenCalled(); + + expect(that.config.controller).toBe('EditLotModalController'); + expect(that.config.controllerAs).toBe('vm'); + expect(that.config.templateUrl).toBe('stock-edit-lot-modal/edit-lot-modal.html'); + expect(that.config.show).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.selectedItem)).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.allLineItems)).toBeTruthy(); + expect(angular.isFunction(that.config.resolve.addedLineItems)).toBeTruthy(); + }); + }); +}); \ No newline at end of file Index: src/stock-edit-lot-modal/messages_en.json =================================================================== diff -u -N --- src/stock-edit-lot-modal/messages_en.json (revision 0) +++ src/stock-edit-lot-modal/messages_en.json (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -0,0 +1,10 @@ +{ + "stockEditLotModal.cancel": "Cancel", + "stockEditLotModal.save": "Save", + "stockEditLotModal.title": "Edit lot", + "stockEditLotModal.lotCode": "Lot code", + "stockEditLotModal.expirationDate": "Expiration date", + "stockEditLotModal.expirationDateInvalid": "Lot expiration date should be a future date.", + "stockEditLotModal.expirationDateInvalidForLotCode": "There exists another expiration date for this code.", + "stockEditLotModal.lotCodeInvalid": "This lot code already exists." +} \ No newline at end of file Index: src/stock-issue-creation/issue-creation.routes.js =================================================================== diff -u -N -r89e13f9bb25d1aba1bce0602bcffa9aef6f2cdbd -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-issue-creation/issue-creation.routes.js (.../issue-creation.routes.js) (revision 89e13f9bb25d1aba1bce0602bcffa9aef6f2cdbd) +++ src/stock-issue-creation/issue-creation.routes.js (.../issue-creation.routes.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -87,6 +87,9 @@ ); } return $stateParams.srcDstAssignments; + }, + hasPermissionToAddNewLot: function() { + return false; } } }); Index: src/stock-orderable-group/messages_en.json =================================================================== diff -u -N -r81fa524ba121f06bef78551e35358811725be7c2 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-orderable-group/messages_en.json (.../messages_en.json) (revision 81fa524ba121f06bef78551e35358811725be7c2) +++ src/stock-orderable-group/messages_en.json (.../messages_en.json) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -1,4 +1,5 @@ { "orderableGroupService.noLotDefined": "No lot defined", - "orderableGroupService.productHasNoLots": "Product has no lots" + "orderableGroupService.productHasNoLots": "Product has no lots", + "orderableGroupService.addMissingLot": "Add new lot" } Index: src/stock-orderable-group/orderable-group.service.js =================================================================== diff -u -N -rc23f766eda510783de2be91d020ad195dc960d64 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-orderable-group/orderable-group.service.js (.../orderable-group.service.js) (revision c23f766eda510783de2be91d020ad195dc960d64) +++ src/stock-orderable-group/orderable-group.service.js (.../orderable-group.service.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -45,6 +45,7 @@ this.findByLotInOrderableGroup = findByLotInOrderableGroup; this.areOrderablesUseVvm = areOrderablesUseVvm; this.getKitOnlyOrderablegroup = getKitOnlyOrderablegroup; + this.addItemWithNewLot = addItemWithNewLot; /** * @ngdoc method @@ -58,30 +59,65 @@ * @param {Object} orderableGroup orderable group * @return {Array} array with lots */ - function lotsOf(orderableGroup) { - var lots = _.chain(orderableGroup).pluck('lot') - .compact() - .value(); + function lotsOf(orderableGroup, addMissingLotAllowed) { + var addMissingLot = { + lotCode: messageService.get('orderableGroupService.addMissingLot') + }, + lots; - lots.forEach(function(lot) { - lot.expirationDate = dateUtils.toDate(lot.expirationDate); - }); + if (orderableGroup && orderableGroup.length > 0 && orderableGroup[0].$allLotsAdded) { + lots = []; + } else { + lots = _.chain(orderableGroup).pluck('lot') + .compact() + .value(); - var someHasLot = lots.length > 0; - var someHasNoLot = _.any(orderableGroup, function(item) { - return !item.lot; - }); + lots.forEach(function(lot) { + lot.expirationDate = dateUtils.toDate(lot.expirationDate); + }); - if (someHasLot && someHasNoLot) { - //add no lot defined as an option - lots.unshift(noLotDefined); + var someHasLot = lots.length > 0, + someHasNoLot = _.any(orderableGroup, function(item) { + return !item.lot; + }); + + if ((addMissingLotAllowed || someHasLot) && someHasNoLot) { + lots.unshift(noLotDefined); + } + sortByFieldName(lots, 'expirationDate'); } + + if (addMissingLotAllowed) { + lots.unshift(addMissingLot); + } return lots; } /** * @ngdoc method * @methodOf stock-orderable-group.orderableGroupService + * @name sortByFieldName + * + * @description + * Sorts array by field name + * + * @param {Object} array array to sort + * @param {Object} fieldName name of the field by which the array is sorted + */ + function sortByFieldName(array, fieldName) { + array.sort(function(a, b) { + if (a[fieldName] < b[fieldName]) { + return -1; + } else if (a[fieldName] > b[fieldName]) { + return 1; + } + return 0; + }); + } + + /** + * @ngdoc method + * @methodOf stock-orderable-group.orderableGroupService * @name determineLotMessage * * @description @@ -91,11 +127,12 @@ * @param {Object} selectedItem product with lot property. Property displayLotMessage * will be assigned to id. */ - function determineLotMessage(selectedItem, orderableGroup) { + function determineLotMessage(selectedItem, orderableGroup, addMissingLotAllowed) { if (selectedItem.lot) { selectedItem.displayLotMessage = selectedItem.lot.lotCode; } else { - var messageKey = lotsOf(orderableGroup).length > 0 ? 'noLotDefined' : 'productHasNoLots'; + var messageKey = lotsOf(orderableGroup, addMissingLotAllowed).length > 0 + ? 'noLotDefined' : 'productHasNoLots'; selectedItem.displayLotMessage = messageService.get('orderableGroupService.' + messageKey); } } @@ -162,15 +199,23 @@ * @param {Object} selectedLot selected lot * @return {Object} found product */ - function findByLotInOrderableGroup(orderableGroup, selectedLot) { + function findByLotInOrderableGroup(orderableGroup, selectedLot, isNewLot) { var selectedItem = _.chain(orderableGroup) .find(function(groupItem) { var selectedNoLot = !groupItem.lot && (!selectedLot || selectedLot === noLotDefined); var lotMatch = groupItem.lot && groupItem.lot === selectedLot; - return selectedNoLot || lotMatch; + return selectedNoLot || lotMatch || isNewLot; }) .value(); + if (isNewLot) { + var copiedSelectedItem = angular.copy(selectedItem); + copiedSelectedItem.lot = selectedLot; + copiedSelectedItem.stockOnHand = 0; + determineLotMessage(copiedSelectedItem, orderableGroup); + return copiedSelectedItem; + } + if (selectedItem) { determineLotMessage(selectedItem, orderableGroup); } @@ -180,6 +225,30 @@ /** * @ngdoc method * @methodOf stock-orderable-group.orderableGroupService + * @name addItemWithNewLot + * + * @description + * Creates new item from similar orderable + lot item and newly created lot. + * + * @param {Object} newLot newly created lot + * @param {Object} similarItem object with orderable field to be used + * @return {Object} item created from passed parameters + */ + function addItemWithNewLot(newLot, similarItem) { + var newItem = angular.copy(similarItem); + + newItem.id = undefined; + newItem.lot = angular.copy(newLot); + newItem.stockOnHand = 0; + newItem.$isNewItem = true; + determineLotMessage(newItem); + + return newItem; + } + + /** + * @ngdoc method + * @methodOf stock-orderable-group.orderableGroupService * @name areOrderablesUseVvm * * @description Index: src/stock-orderable-group/orderable-group.service.spec.js =================================================================== diff -u -N -r5a0769bd03716dd9d38c76e051bb8edf1c9e02f5 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-orderable-group/orderable-group.service.spec.js (.../orderable-group.service.spec.js) (revision 5a0769bd03716dd9d38c76e051bb8edf1c9e02f5) +++ src/stock-orderable-group/orderable-group.service.spec.js (.../orderable-group.service.spec.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -40,9 +40,21 @@ this.LotDataBuilder = $injector.get('LotDataBuilder'); this.OrderableChildrenDataBuilder = $injector.get('OrderableChildrenDataBuilder'); this.OrderableGroupDataBuilder = $injector.get('OrderableGroupDataBuilder'); + this.orderableGroupService = $injector.get('orderableGroupService'); }); - this.lot1 = new this.LotDataBuilder().build(); + this.lot1 = { + id: 'lot id 1', + expirationDate: '2022-05-08' + }; + this.lot2 = { + id: 'lot id 2', + expirationDate: '2019-01-20' + }; + this.lot3 = { + id: 'lot id 3', + expirationDate: '2018-04-03' + }; this.item1 = { orderable: { @@ -60,6 +72,19 @@ id: 'b' } }; + this.item4 = { + orderable: { + id: 'a' + }, + lot: this.lot2 + }; + this.item5 = { + orderable: { + id: 'a' + }, + lot: this.lot3 + }; + this.items = [this.item1, this.item2, this.item3]; this.kitConstituents = [ new this.OrderableChildrenDataBuilder().withId('child_product_1_id') @@ -83,39 +108,27 @@ }); it('should group items by orderable id', function() { - //given - var items = [this.item1, this.item2, this.item3]; - - //when - var groups = service.groupByOrderableId(items); - - //then - expect(groups).toEqual([ + expect(this.orderableGroupService.groupByOrderableId(this.items)).toEqual([ [this.item1, this.item2], [this.item3] ]); }); it('should find item in group by lot', function() { - //given - var items = [this.item1, this.item2, this.item3]; - - //when - var found = service.findByLotInOrderableGroup(items, this.lot1); - - //then - expect(found).toBe(this.item1); + expect(this.orderableGroupService.findByLotInOrderableGroup(this.items, this.lot1)).toBe(this.item1); }); it('should find item in group by NULL lot', function() { - //given - var items = [this.item1, this.item2, this.item3]; + expect(this.orderableGroupService.findByLotInOrderableGroup(this.items, null)).toBe(this.item2); + }); - //when - var found = service.findByLotInOrderableGroup(items, null); + it('should find item with new lot', function() { + var newLot = new this.LotDataBuilder().build(), + newItem = this.item2; + newItem.lot = newLot; + newItem.stockOnHand = 0; - //then - expect(found).toBe(this.item2); + expect(this.orderableGroupService.findByLotInOrderableGroup(this.items, newLot)).toBe(newItem); }); it('should find lots in orderable group', function() { @@ -132,9 +145,32 @@ expect(lots[1]).toEqual(this.lot1); expect(lots[1].expirationDate.toString()) - .toEqual('Tue May 02 2017 05:59:51 GMT+0000 (Coordinated Universal Time)'); + .toEqual('Sun May 08 2022 00:00:00 GMT+0000 (Coordinated Universal Time)'); }); + it('should add option to add missing lot if is allowed', function() { + var group = [this.item1, this.item4], + lots = this.orderableGroupService.lotsOf(group, true); + + expect(lots[0]).toEqual({ + lotCode: 'orderableGroupService.addMissingLot' + }); + + expect(lots[1]).toEqual(this.lot2); + + expect(lots[1].expirationDate.toString()) + .toEqual('Sun Jan 20 2019 00:00:00 GMT+0000 (Coordinated Universal Time)'); + }); + + it('should sort lots by filed expirationDate', function() { + var group = [this.item1, this.item4, this.item5], + lots = this.orderableGroupService.lotsOf(group, true); + + expect(lots).toEqual([{ + lotCode: 'orderableGroupService.addMissingLot' + }, this.lot3, this.lot2, this.lot1]); + }); + it('should return kit only orderableGroups', function() { var item = service.getKitOnlyOrderablegroup(this.orderableGroups); Index: src/stock-physical-inventory-draft/messages_en.json =================================================================== diff -u -N -r19893230364f711560a71cbcbbcf38d0841c9fab -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-physical-inventory-draft/messages_en.json (.../messages_en.json) (revision 19893230364f711560a71cbcbbcf38d0841c9fab) +++ src/stock-physical-inventory-draft/messages_en.json (.../messages_en.json) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -12,6 +12,7 @@ "stockPhysicalInventoryDraft.productWithDisplayUnit": "${fullProductName} - ${displayUnit}", "stockPhysicalInventoryDraft.productCode": "Product Code", "stockPhysicalInventoryDraft.lotCode": "Lot Code", + "stockPhysicalInventoryDraft.lotCodeMustBeUnique": "Lot code must be unique", "stockPhysicalInventoryDraft.expiryDate": "Expiry Date", "stockPhysicalInventoryDraft.soh": "Stock on Hand", "stockPhysicalInventoryDraft.currentStock": "Current Stock", Index: src/stock-physical-inventory-draft/physical-inventory-draft.controller.js =================================================================== diff -u -N -r19893230364f711560a71cbcbbcf38d0841c9fab -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-physical-inventory-draft/physical-inventory-draft.controller.js (.../physical-inventory-draft.controller.js) (revision 19893230364f711560a71cbcbbcf38d0841c9fab) +++ src/stock-physical-inventory-draft/physical-inventory-draft.controller.js (.../physical-inventory-draft.controller.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -34,15 +34,17 @@ 'displayLineItemsGroup', 'confirmService', 'physicalInventoryService', 'MAX_INTEGER_VALUE', 'VVM_STATUS', 'reasons', 'stockReasonsCalculations', 'loadingModalService', '$window', 'stockmanagementUrlFactory', 'accessTokenFactory', 'orderableGroupService', '$filter', '$q', - 'offlineService', 'physicalInventoryDraftCacheService', 'stockCardService']; + 'offlineService', 'physicalInventoryDraftCacheService', 'stockCardService', 'LotResource', + 'editLotModalService']; function controller($scope, $state, $stateParams, addProductsModalService, messageService, physicalInventoryFactory, notificationService, alertService, chooseDateModalService, program, facility, draft, displayLineItemsGroup, confirmService, physicalInventoryService, MAX_INTEGER_VALUE, VVM_STATUS, reasons, stockReasonsCalculations, loadingModalService, $window, stockmanagementUrlFactory, accessTokenFactory, orderableGroupService, $filter, $q, - offlineService, physicalInventoryDraftCacheService, stockCardService) { + offlineService, physicalInventoryDraftCacheService, stockCardService, + LotResource, editLotModalService) { var vm = this; vm.$onInit = onInit; @@ -218,7 +220,33 @@ .difference(_.flatten(vm.displayLineItemsGroup)) .value(); - addProductsModalService.show(notYetAddedItems, vm.hasLot).then(function() { + var orderablesWithoutAvailableLots = draft.lineItems.map(function(item) { + return item.orderable; + }).filter(function(orderable) { + return !notYetAddedItems.find(function(item) { + return orderable.id === item.orderable.id; + }); + }) + .filter(function(orderable, index, filtered) { + return filtered.indexOf(orderable) === index; + }) + .map(function(uniqueOrderable) { + return { + lot: null, + orderable: uniqueOrderable, + quantity: null, + stockAdjustments: [], + stockOnHand: null, + vvmStatus: null, + $allLotsAdded: true + }; + }); + + orderablesWithoutAvailableLots.forEach(function(item) { + notYetAddedItems.push(item); + }); + + addProductsModalService.show(notYetAddedItems, draft.lineItems).then(function() { $stateParams.program = vm.program; $stateParams.facility = vm.facility; $stateParams.noReload = true; @@ -236,6 +264,23 @@ /** * @ngdoc method * @methodOf stock-physical-inventory-draft.controller:PhysicalInventoryDraftController + * @name editLot + * + * @description + * Pops up a modal for users to edit lot for selected line item. + * + * @param {Object} lineItem line items to be edited. + */ + vm.editLot = function(lineItem) { + var addedLineItems = _.flatten(draft.lineItems); + editLotModalService.show(lineItem, addedLineItems).then(function() { + $stateParams.draft = draft; + }); + }; + + /** + * @ngdoc method + * @methodOf stock-physical-inventory-draft.controller:PhysicalInventoryDraftController * @name calculate * * @description @@ -344,6 +389,11 @@ $stateParams.program = vm.program; $stateParams.facility = vm.facility; + draft.lineItems.forEach(function(lineItem) { + if (lineItem.$isNewItem) { + lineItem.$isNewItem = false; + } + }); $stateParams.noReload = true; $state.go($state.current.name, $stateParams, { @@ -358,6 +408,20 @@ /** * @ngdoc method * @methodOf stock-physical-inventory-draft.controller:PhysicalInventoryDraftController + * @name canEditLot + * + * @description + * Checks if user can edit lot if it was created during inventory + * + * @param {Object} lineItem line item to edit + */ + vm.canEditLot = function(lineItem) { + return lineItem.lot && lineItem.$isNewItem; + }; + + /** + * @ngdoc method + * @methodOf stock-physical-inventory-draft.controller:PhysicalInventoryDraftController * @name saveOnPageChange * * @description @@ -413,36 +477,86 @@ draft.occurredDate = resolvedData.occurredDate; draft.signature = resolvedData.signature; - physicalInventoryService.submitPhysicalInventory(draft).then(function() { - if (validate(draft.lineItems)) { - loadingModalService.close(); - $scope.$broadcast('openlmis-form-submit'); - alertService.error('stockPhysicalInventoryDraft.submitInvalidActive'); - } else { - notificationService.success('stockPhysicalInventoryDraft.submitted'); - confirmService.confirm('stockPhysicalInventoryDraft.printModal.label', - 'stockPhysicalInventoryDraft.printModal.yes', - 'stockPhysicalInventoryDraft.printModal.no') - .then(function() { - $window.open(accessTokenFactory.addAccessToken(getPrintUrl(draft.id)), '_blank'); - }) - .finally(function() { - $state.go('openlmis.stockmanagement.stockCardSummaries', { - program: program.id, - facility: facility.id, - includeInactive: false + return saveLots(draft, function() { + physicalInventoryService.submitPhysicalInventory(draft).then(function() { + if (validate(draft.lineItems)) { + loadingModalService.close(); + $scope.$broadcast('openlmis-form-submit'); + alertService.error('stockPhysicalInventoryDraft.submitInvalidActive'); + } else { + notificationService.success('stockPhysicalInventoryDraft.submitted'); + confirmService.confirm('stockPhysicalInventoryDraft.printModal.label', + 'stockPhysicalInventoryDraft.printModal.yes', + 'stockPhysicalInventoryDraft.printModal.no') + .then(function() { + $window.open(accessTokenFactory.addAccessToken(getPrintUrl(draft.id)), + '_blank'); + }) + .finally(function() { + $state.go('openlmis.stockmanagement.stockCardSummaries', { + program: program.id, + facility: facility.id, + includeInactive: false + }); }); - }); - } - }, function(errorResponse) { - loadingModalService.close(); - alertService.error(errorResponse.data.message); - physicalInventoryDraftCacheService.removeById(draft.id); + } + }, function(errorResponse) { + loadingModalService.close(); + alertService.error(errorResponse.data.message); + physicalInventoryDraftCacheService.removeById(draft.id); + }); }); }); } }; + function saveLots(draft, submitMethod) { + var lotPromises = [], + lotResource = new LotResource(), + errorLots = []; + + draft.lineItems.forEach(function(lineItem) { + if (lineItem.lot && lineItem.$isNewItem && !lineItem.lot.id) { + lotPromises.push(lotResource.create(lineItem.lot) + .then(function(createResponse) { + lineItem.$isNewItem = false; + return createResponse; + }) + .catch(function(response) { + if (response.data.messageKey === + 'referenceData.error.lot.lotCode.mustBeUnique') { + errorLots.push(lineItem.lot.lotCode); + } + })); + } + }); + + return $q.all(lotPromises) + .then(function(responses) { + if (errorLots !== undefined && errorLots.length > 0) { + return $q.reject(); + } + responses.forEach(function(lot) { + draft.lineItems.forEach(function(lineItem) { + if (lineItem.lot && lineItem.lot.lotCode === lot.lotCode) { + lineItem.lot = lot; + } + }); + return draft.lineItems; + }); + return submitMethod(); + }) + .catch(function(errorResponse) { + loadingModalService.close(); + if (errorLots) { + alertService.error('stockPhysicalInventoryDraft.lotCodeMustBeUnique', + errorLots.join(', ')); + return $q.reject(errorResponse.data.message); + } + alertService.error(errorResponse.data.message); + }); + } + /** * @ngdoc method * @methodOf stock-adjustment-creation.controller:StockAdjustmentCreationController Index: src/stock-physical-inventory-draft/physical-inventory-draft.controller.spec.js =================================================================== diff -u -N -rb4e10952cc208eb13abcedc3f62eb2ad3167db72 -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-physical-inventory-draft/physical-inventory-draft.controller.spec.js (.../physical-inventory-draft.controller.spec.js) (revision b4e10952cc208eb13abcedc3f62eb2ad3167db72) +++ src/stock-physical-inventory-draft/physical-inventory-draft.controller.spec.js (.../physical-inventory-draft.controller.spec.js) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -49,6 +49,8 @@ this.alertService = $injector.get('alertService'); this.stockCardService = $injector.get('stockCardService'); this.loadingModalService = $injector.get('loadingModalService'); + this.LotResource = $injector.get('LotResource'); + this.editLotModalService = $injector.get('editLotModalService'); }); spyOn(this.physicalInventoryService, 'submitPhysicalInventory'); @@ -61,6 +63,7 @@ spyOn(this.physicalInventoryDraftCacheService, 'cacheDraft'); spyOn(this.alertService, 'error'); spyOn(this.stockCardService, 'deactivateStockCard'); + spyOn(this.editLotModalService, 'show'); this.program = new this.ProgramDataBuilder() .withId('1') @@ -158,6 +161,7 @@ ], draft: this.draft, addProductsModalService: this.addProductsModalService, + editLotModalService: this.editLotModalService, chooseDateModalService: chooseDateModalService, reasons: this.reasons, physicalInventoryService: this.physicalInventoryService, @@ -241,16 +245,34 @@ this.vm.addProducts(); - expect(this.addProductsModalService.show).toHaveBeenCalledWith([this.lineItem2, this.lineItem4], true); + expect(this.addProductsModalService.show).toHaveBeenCalledWith([ + this.lineItem2, + this.lineItem4, + asd(this.lineItem1.orderable), + asd(this.lineItem3.orderable) + ], [this.lineItem1, this.lineItem2, this.lineItem3, this.lineItem4]); }); + function asd(orderable) { + return { + lot: null, + orderable: orderable, + quantity: null, + stockAdjustments: [], + stockOnHand: null, + vvmStatus: null, + $allLotsAdded: true + }; + } + describe('saveDraft', function() { it('should save draft', function() { this.draftFactory.saveDraft.andReturn(this.$q.defer().promise); - this.$rootScope.$apply(); + this.draftFactory.saveDraft.andReturn(this.$q.resolve()); this.vm.saveDraft(); + this.$rootScope.$apply(); expect(this.draftFactory.saveDraft).toHaveBeenCalledWith(this.draft); }); Index: src/stock-physical-inventory-draft/physical-inventory-draft.html =================================================================== diff -u -N -r19893230364f711560a71cbcbbcf38d0841c9fab -r4ab1e5e0a5a657ca718bcd646162990cd5f5ada5 --- src/stock-physical-inventory-draft/physical-inventory-draft.html (.../physical-inventory-draft.html) (revision 19893230364f711560a71cbcbbcf38d0841c9fab) +++ src/stock-physical-inventory-draft/physical-inventory-draft.html (.../physical-inventory-draft.html) (revision 4ab1e5e0a5a657ca718bcd646162990cd5f5ada5) @@ -43,8 +43,8 @@ {{'stockPhysicalInventoryDraft.productCode' | message}} {{'stockPhysicalInventoryDraft.product' | message}} - {{'stockPhysicalInventoryDraft.lotCode' | message}} - {{'stockPhysicalInventoryDraft.expiryDate' | message}} + {{'stockPhysicalInventoryDraft.lotCode' | message}} + {{'stockPhysicalInventoryDraft.expiryDate' | message}} {{'stockPhysicalInventoryDraft.soh' | message}} {{'stockPhysicalInventoryDraft.currentStock' | message}} {{'stockPhysicalInventoryDraft.VVMStatus' | message}} @@ -69,8 +69,15 @@ {{lineItems.length > 1 ? '' : lineItem.orderable.productCode}} {{lineItems.length > 1 ? '' : (lineItem.orderable | productName)}} - {{lineItem.displayLotMessage}} - {{lineItem.lot.expirationDate | openlmisDate}} + + + + + {{lineItem.displayLotMessage}} + + {{lineItem.lot.expirationDate | openlmisDate}} {{lineItem.stockOnHand}}