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 @@