From 52f35ea0bae97b9151765307330122d4dbb3b274 Mon Sep 17 00:00:00 2001 From: Denys Mamchura Date: Thu, 23 Jul 2026 16:15:10 +0200 Subject: [PATCH 1/2] OLMIS-8249: Mark invalid adjustment quantity fields Reasons whose quantity is not greater than 0 block the physical inventory update, but the offending fields were not marked, so users could not tell which line was blocking them. Expose hasValidQuantity on the AdjustmentsModalController and add a quantity-invalid class to an adjustment row when its quantity is not greater than 0. The invalid state is rendered on the whole quantity field (the .input-control wrapper), matching how the Reason field is marked. The selector targets the wrapper
only: a bare .input-control also matches the inner (which @extends .input-control) and would paint a second, nested border. Covered by new AdjustmentsModalController hasValidQuantity specs. --- .../_adjustments-modal.scss | 11 ++++ .../adjustments-modal.controller.js | 13 +++++ .../adjustments-modal.controller.spec.js | 56 +++++++++++++++++++ .../adjustments-modal.html | 6 +- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/openlmis-adjustments/_adjustments-modal.scss b/src/openlmis-adjustments/_adjustments-modal.scss index 00cab424..c727d5ec 100644 --- a/src/openlmis-adjustments/_adjustments-modal.scss +++ b/src/openlmis-adjustments/_adjustments-modal.scss @@ -19,4 +19,15 @@ form { max-width: 100%; } + + // The design-system wraps each input in an .input-control
. Mark the + // whole quantity field (the wrapper) as invalid — the same way the Reason + // field is marked — for reasons whose quantity is not greater than 0 and + // therefore block the update. The selector is restricted to the wrapper + //
; a bare `.input-control` would also hit the inner (which + // `@extend`s .input-control) and paint a second, nested border. + td.quantity-invalid div.input-control { + border: 1px solid $brand-danger; + box-shadow: inset 0 0 0.5em $brand-danger; + } } diff --git a/src/openlmis-adjustments/adjustments-modal.controller.js b/src/openlmis-adjustments/adjustments-modal.controller.js index c37ef66c..134ed64e 100644 --- a/src/openlmis-adjustments/adjustments-modal.controller.js +++ b/src/openlmis-adjustments/adjustments-modal.controller.js @@ -47,6 +47,7 @@ vm.cancel = cancel; vm.save = save; vm.hasInvalidAdjustments = hasInvalidAdjustments; + vm.hasValidQuantity = hasValidQuantity; /** * @ngdoc property @@ -269,6 +270,18 @@ }); } + /** + * @ngdoc method + * @methodOf openlmis-adjustments.controller:AdjustmentsModalController + * @name hasValidQuantity + * + * @description + * Checks whether a single adjustment has a quantity greater than zero. Used both to block + * saving and to mark the offending quantity input as invalid in the adjustments table. + * + * @param {Object} adjustment the adjustment to validate + * @return {Boolean} true if the adjustment quantity is greater than zero + */ function hasValidQuantity(adjustment) { return adjustment !== undefined && adjustment !== null diff --git a/src/openlmis-adjustments/adjustments-modal.controller.spec.js b/src/openlmis-adjustments/adjustments-modal.controller.spec.js index 8a65ff56..eb3960a3 100644 --- a/src/openlmis-adjustments/adjustments-modal.controller.spec.js +++ b/src/openlmis-adjustments/adjustments-modal.controller.spec.js @@ -427,6 +427,62 @@ describe('AdjustmentsModalController', function() { }); + describe('hasValidQuantity', function() { + + beforeEach(function() { + this.initController(); + }); + + it('should return true when quantity is greater than zero', function() { + expect(this.vm.hasValidQuantity({ + quantity: 5 + })).toBe(true); + }); + + it('should return false when quantity is zero', function() { + expect(this.vm.hasValidQuantity({ + quantity: 0 + })).toBe(false); + }); + + it('should return false when quantity is negative', function() { + expect(this.vm.hasValidQuantity({ + quantity: -5 + })).toBe(false); + }); + + it('should return false when quantity is empty', function() { + expect(this.vm.hasValidQuantity({ + quantity: '' + })).toBe(false); + }); + + it('should return false when quantity is null', function() { + expect(this.vm.hasValidQuantity({ + quantity: null + })).toBe(false); + }); + + it('should return false when quantity is undefined', function() { + expect(this.vm.hasValidQuantity({})).toBe(false); + }); + + it('should return false when quantity is not a number', function() { + expect(this.vm.hasValidQuantity({ + quantity: 'abc' + })).toBe(false); + }); + + it('should return false when adjustment is undefined', function() { + expect(this.vm.hasValidQuantity(undefined)).toBe(false); + }); + + it('should return false when adjustment is null', function() { + expect(this.vm.hasValidQuantity(null)).toBe(false); + }); + + }); + function initController() { this.vm = this.$controller('AdjustmentsModalController', { modalDeferred: this.modalDeferred, diff --git a/src/openlmis-adjustments/adjustments-modal.html b/src/openlmis-adjustments/adjustments-modal.html index 9113f168..8789426e 100644 --- a/src/openlmis-adjustments/adjustments-modal.html +++ b/src/openlmis-adjustments/adjustments-modal.html @@ -48,10 +48,10 @@

{{vm.title | message}}

{{adjustment.reason.name}} - +
{{adjustment.quantity}}
- From e65e54e85b281ee3f5b30b0d556186869f3e4bb5 Mon Sep 17 00:00:00 2001 From: Denys Mamchura Date: Fri, 24 Jul 2026 15:54:27 +0200 Subject: [PATCH 2/2] OLMIS-8249: Reuse openlmis-invalid to mark invalid adjustment lines Per review, replace the bespoke invalid style with the platform's existing openlmis-invalid mechanism, the same pattern used by stock-add-products-modal. - Drop the module-scoped SCSS rule and the custom quantity-invalid class. - validateAdjustment stores a quantityInvalid message on an adjustment whose quantity is not greater than 0; it runs on quantity change and for every row on save. - Mark the quantity cell with openlmis-invalid, and broadcast openlmis-form-submit on save so the messages are revealed. No custom CSS. Works in both the Packs and the Doses view. Specs updated to cover validateAdjustment and save marking. --- .../_adjustments-modal.scss | 11 -- .../adjustments-modal.controller.js | 30 +++-- .../adjustments-modal.controller.spec.js | 120 ++++++++++++++---- .../adjustments-modal.html | 6 +- 4 files changed, 116 insertions(+), 51 deletions(-) diff --git a/src/openlmis-adjustments/_adjustments-modal.scss b/src/openlmis-adjustments/_adjustments-modal.scss index c727d5ec..00cab424 100644 --- a/src/openlmis-adjustments/_adjustments-modal.scss +++ b/src/openlmis-adjustments/_adjustments-modal.scss @@ -19,15 +19,4 @@ form { max-width: 100%; } - - // The design-system wraps each input in an .input-control
. Mark the - // whole quantity field (the wrapper) as invalid — the same way the Reason - // field is marked — for reasons whose quantity is not greater than 0 and - // therefore block the update. The selector is restricted to the wrapper - //
; a bare `.input-control` would also hit the inner (which - // `@extend`s .input-control) and paint a second, nested border. - td.quantity-invalid div.input-control { - border: 1px solid $brand-danger; - box-shadow: inset 0 0 0.5em $brand-danger; - } } diff --git a/src/openlmis-adjustments/adjustments-modal.controller.js b/src/openlmis-adjustments/adjustments-modal.controller.js index 134ed64e..5c18d550 100644 --- a/src/openlmis-adjustments/adjustments-modal.controller.js +++ b/src/openlmis-adjustments/adjustments-modal.controller.js @@ -29,15 +29,15 @@ .controller('AdjustmentsModalController', AdjustmentsModalController); AdjustmentsModalController.$inject = [ - '$filter', '$q', 'modalDeferred', 'title', 'message', 'isDisabled', 'adjustments', + '$filter', '$q', '$scope', 'modalDeferred', 'title', 'message', 'isDisabled', 'adjustments', 'reasons', 'summaries', 'preSave', 'preCancel', 'filterReasons', 'showInDoses', 'lineItem', - 'quantityUnitCalculateService', 'alertService' + 'quantityUnitCalculateService', 'alertService', 'messageService' ]; - function AdjustmentsModalController($filter, $q, modalDeferred, title, message, isDisabled, + function AdjustmentsModalController($filter, $q, $scope, modalDeferred, title, message, isDisabled, adjustments, reasons, summaries, preSave, preCancel, filterReasons, showInDoses, lineItem, quantityUnitCalculateService, - alertService) { + alertService, messageService) { var vm = this; @@ -47,7 +47,7 @@ vm.cancel = cancel; vm.save = save; vm.hasInvalidAdjustments = hasInvalidAdjustments; - vm.hasValidQuantity = hasValidQuantity; + vm.validateAdjustment = validateAdjustment; /** * @ngdoc property @@ -231,7 +231,10 @@ * back to the modal without applying any changes; */ function save() { + angular.forEach(vm.adjustments, validateAdjustment); + if (hasInvalidAdjustments()) { + $scope.$broadcast('openlmis-form-submit'); alertService.error('openlmisAdjustments.quantityGreaterThanZero'); return; } @@ -273,15 +276,22 @@ /** * @ngdoc method * @methodOf openlmis-adjustments.controller:AdjustmentsModalController - * @name hasValidQuantity + * @name validateAdjustment * * @description - * Checks whether a single adjustment has a quantity greater than zero. Used both to block - * saving and to mark the offending quantity input as invalid in the adjustments table. + * Validates a single adjustment and stores an error message on its quantityInvalid property + * when the quantity is not greater than zero. The message is consumed by the openlmis-invalid + * directive on the quantity cell, marking the reasons that block the adjustments update - the + * same mechanism used by the stock add products modal. * - * @param {Object} adjustment the adjustment to validate - * @return {Boolean} true if the adjustment quantity is greater than zero + * @param {Object} adjustment the adjustment to validate */ + function validateAdjustment(adjustment) { + adjustment.quantityInvalid = hasValidQuantity(adjustment) + ? undefined + : messageService.get('openlmisAdjustments.quantityGreaterThanZero'); + } + function hasValidQuantity(adjustment) { return adjustment !== undefined && adjustment !== null diff --git a/src/openlmis-adjustments/adjustments-modal.controller.spec.js b/src/openlmis-adjustments/adjustments-modal.controller.spec.js index eb3960a3..32b17511 100644 --- a/src/openlmis-adjustments/adjustments-modal.controller.spec.js +++ b/src/openlmis-adjustments/adjustments-modal.controller.spec.js @@ -23,6 +23,13 @@ describe('AdjustmentsModalController', function() { this.$rootScope = $injector.get('$rootScope'); this.$controller = $injector.get('$controller'); this.alertService = $injector.get('alertService'); + this.messageService = $injector.get('messageService'); + }); + + this.$scope = this.$rootScope.$new(); + + spyOn(this.messageService, 'get').andCallFake(function(key) { + return key; }); this.modalDeferred = this.$q.defer(); @@ -427,64 +434,121 @@ describe('AdjustmentsModalController', function() { }); - describe('hasValidQuantity', function() { + describe('validateAdjustment', function() { beforeEach(function() { this.initController(); }); - it('should return true when quantity is greater than zero', function() { - expect(this.vm.hasValidQuantity({ - quantity: 5 - })).toBe(true); + it('should clear quantityInvalid when quantity is greater than zero', function() { + var adjustment = { + quantity: 5, + quantityInvalid: 'openlmisAdjustments.quantityGreaterThanZero' + }; + + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBeUndefined(); }); - it('should return false when quantity is zero', function() { - expect(this.vm.hasValidQuantity({ + it('should set quantityInvalid message when quantity is zero', function() { + var adjustment = { quantity: 0 - })).toBe(false); + }; + + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBe('openlmisAdjustments.quantityGreaterThanZero'); }); - it('should return false when quantity is negative', function() { - expect(this.vm.hasValidQuantity({ + it('should set quantityInvalid message when quantity is negative', function() { + var adjustment = { quantity: -5 - })).toBe(false); + }; + + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBe('openlmisAdjustments.quantityGreaterThanZero'); }); - it('should return false when quantity is empty', function() { - expect(this.vm.hasValidQuantity({ + it('should set quantityInvalid message when quantity is empty', function() { + var adjustment = { quantity: '' - })).toBe(false); - }); + }; - it('should return false when quantity is null', function() { - expect(this.vm.hasValidQuantity({ - quantity: null - })).toBe(false); + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBe('openlmisAdjustments.quantityGreaterThanZero'); }); - it('should return false when quantity is undefined', function() { - expect(this.vm.hasValidQuantity({})).toBe(false); + it('should set quantityInvalid message when quantity is undefined', function() { + var adjustment = {}; + + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBe('openlmisAdjustments.quantityGreaterThanZero'); }); - it('should return false when quantity is not a number', function() { - expect(this.vm.hasValidQuantity({ + it('should set quantityInvalid message when quantity is not a number', function() { + var adjustment = { quantity: 'abc' - })).toBe(false); + }; + + this.vm.validateAdjustment(adjustment); + + expect(adjustment.quantityInvalid).toBe('openlmisAdjustments.quantityGreaterThanZero'); }); - it('should return false when adjustment is undefined', function() { - expect(this.vm.hasValidQuantity(undefined)).toBe(false); + }); + + describe('save marking', function() { + + it('should mark every invalid adjustment when saving', function() { + var message = 'openlmisAdjustments.quantityGreaterThanZero'; + + this.adjustments = [{ + reason: this.reasons[0], + quantity: 0 + }, { + reason: this.reasons[1], + quantity: 5 + }, { + reason: this.reasons[2], + quantity: '' + }]; + + this.initController(); + this.vm.$onInit(); + + this.vm.save(); + + expect(this.vm.adjustments[0].quantityInvalid).toBe(message); + expect(this.vm.adjustments[1].quantityInvalid).toBeUndefined(); + expect(this.vm.adjustments[2].quantityInvalid).toBe(message); }); - it('should return false when adjustment is null', function() { - expect(this.vm.hasValidQuantity(null)).toBe(false); + it('should broadcast openlmis-form-submit when saving invalid adjustments', function() { + spyOn(this.$scope, '$broadcast').andCallThrough(); + + this.adjustments = [{ + reason: this.reasons[0], + quantity: 0 + }]; + + this.initController(); + this.vm.$onInit(); + + this.vm.save(); + + expect(this.$scope.$broadcast).toHaveBeenCalledWith('openlmis-form-submit'); }); }); function initController() { this.vm = this.$controller('AdjustmentsModalController', { + $scope: this.$scope, + messageService: this.messageService, modalDeferred: this.modalDeferred, adjustments: this.adjustments, reasons: this.reasons, diff --git a/src/openlmis-adjustments/adjustments-modal.html b/src/openlmis-adjustments/adjustments-modal.html index 8789426e..fd852122 100644 --- a/src/openlmis-adjustments/adjustments-modal.html +++ b/src/openlmis-adjustments/adjustments-modal.html @@ -48,12 +48,14 @@

{{vm.title | message}}

{{adjustment.reason.name}} - +
{{adjustment.quantity}}
+ net-content="vm.lineItem.orderable.netContent" + on-change-quantity="vm.validateAdjustment(adjustment)" + input-class="{'error': adjustment.quantityInvalid}"/>