Skip to content

Commit

Permalink
wip(stock): add pending actions log
Browse files Browse the repository at this point in the history
This commit adds the pending actions log with details on the grid
validation.  It also tears out the stock_value dependency on the data
loaded by the grid - this can be computed in real time as data is being
inserted into the database.
  • Loading branch information
jniles committed Mar 4, 2022
1 parent adf7adb commit 4f96dfe
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 165 deletions.
8 changes: 6 additions & 2 deletions client/src/i18n/en/stock.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,19 @@
"DAYS" : "Days",
"MONTHS" : "Months"
},
"PENDING_ACTIONS" : "Pending Actions",
"MESSAGES" : {
"INFO_NO_EXIT_TYPE" : "No exit type specified. Please select an exit type from the panels above to continue.",
"INFO_NEEDS_LOTS" : "Exit type set has been set. Please click \"Add\" to add rows to the grid.",
"INFO_QUANTITY_IN_STOCK" : "This depot has {{numInventoryItems}} inventory items in {{numLotItems}} lots.",
"SUCCESS_FILLED_N_ITEMS" : "Automatically selected {{count}} items from stock to fulfill requisition. Please check the lot numbers to ensure they are correct as you distribute the items.",
"WARN_PAST_DATE" : "You have chosen a date in the past. Only the lots available on the chosen date will be visible as well as the quantity on this date.",
"WARN_NOT_CONSUMABLE_INVOICE" : "The invoice does not have any consumable items in it. No lots have been automatically filled.",
"WARN_INSUFFICIENT_QUANTITY" : "There is not enough stock of {{hrText}} to fulfill to requisition. The requisition will be partially filled.",
"WARN_OUT_OF_STOCK_QUANTITY" : "There is no stock of {{hrText}} to fill to requisition. This item has been skipped.",
"ERR_NO_DESTINATION" : "No destination is defined for this exit type. Please choose a destination to go with the exit type."
"ERR_NO_DESTINATION" : "No destination is defined for this exit type. Please choose a destination to go with the exit type.",
"ERR_LOT_ERRORS" : "There are errors in the lots grid to be addressed.",
"SUCCESS_FILLED_N_ITEMS" : "Automatically selected {{count}} items from stock to fulfill requisition. Please check the lot numbers to ensure they are correct as you distribute the items.",
"SUCCESS_ALL_VALID" : "Looks good for submission!"
},
"DEPOT_DISTRIBUTION" : "Depot Distribution",
"DIRECTION": " Movement direction",
Expand Down
1 change: 1 addition & 0 deletions client/src/modules/purchases/create/createUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ function PurchaseOrderController(
inventUuids.push(invUUID);
return false;
});

if (dupItem) {
dupItem._valid = false;
dupItem._invalid = true;
Expand Down
1 change: 1 addition & 0 deletions client/src/modules/stock/LotItem.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function LotItemService(uuid, $translate) {
this.text = null;
this.group_uuid = null;
this.unit = null;
this.unit_cost = null;

// lot properties
this.lot_uuid = null;
Expand Down
121 changes: 83 additions & 38 deletions client/src/modules/stock/StockExitForm.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ angular.module('bhima.services')
StockExitFormService.$inject = [
'Store', 'AppCache', 'SessionService', '$timeout', 'bhConstants', 'DepotService',
'Pool', 'LotItemService', 'StockExitFormHelperService', 'util', '$translate',
'StockService',
];

/**
Expand All @@ -12,7 +13,11 @@ StockExitFormService.$inject = [
* @description
* This form powers the stock exit form in BHIMA.
*/
function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, Depots, Pool, Lot, Helpers, util, $translate) {
function StockExitFormService(
Store, AppCache, Session, $timeout, bhConstants,
Depots, Pool, Lot, Helpers, util, $translate,
Stock,
) {

const {
TO_PATIENT, TO_LOSS, TO_SERVICE, TO_OTHER_DEPOT,
Expand All @@ -21,12 +26,14 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
const today = new Date();

const INFO_NO_EXIT_TYPE = 'STOCK.MESSAGES.INFO_NO_EXIT_TYPE';
const INFO_NEEDS_LOTS = 'STOCK.MESSAGES.INFO_NEEDS_LOTS';
const SUCCESS_FILLED_N_ITEMS = 'STOCK.MESSAGES.SUCCESS_FILLED_N_ITEMS';
const WARN_PAST_DATE = 'STOCK.MESSAGES.WARN_PAST_DATE';
const WARN_NOT_CONSUMABLE_INVOICE = 'STOCK.MESSAGES.WARN_NOT_CONSUMABLE_INVOICE';
const WARN_INSUFFICIENT_QUANTITY = 'STOCK.MESSAGES.WARN_INSUFFICIENT_QUANTITY';
const WARN_OUT_OF_STOCK_QUANTITY = 'STOCK.MESSAGES.WARN_OUT_OF_STOCK_QUANTITY';
const ERR_NO_DESTINATION = 'STOCK.MESSGES.ERR_NO_DESTINATION';
const ERR_NO_DESTINATION = 'STOCK.MESSAGES.ERR_NO_DESTINATION';
const ERR_LOT_ERRORS = 'STOCK.MESSAGES.ERR_LOT_ERRORS';

/**
* @constructor
Expand All @@ -50,15 +57,17 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
this._errors = [];
}

function toggleInfoMessage(shouldShowMsg, msgType, msgText, msgKeys = {}) {
StockExitForm.prototype._toggleInfoMessage = function _toggleInfoMessage(
shouldShowMsg, msgType, msgText, msgKeys = {},
) {
// the first thing we do is remove the previous message if it exists. This makes sure that
// we will refresh the view as needed.
this._messages.delete(msgText);

if (shouldShowMsg) {
this._messages.add(msgText, { type : msgType, text : msgText, keys : msgKeys });
this._messages.set(msgText, { type : msgType, text : msgText, keys : msgKeys });
}
}
};

/**
* @method setup
Expand All @@ -78,7 +87,7 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
};

// show the informational message that we need to select an exit type.
toggleInfoMessage(true, 'info', INFO_NO_EXIT_TYPE, this.details);
this._toggleInfoMessage(true, 'info', INFO_NO_EXIT_TYPE, this.details);
};

/**
Expand All @@ -92,7 +101,7 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
StockExitForm.prototype.messages = function messages() {

// the display ordering in the message pane
const order = ['info', 'warn', 'success', 'danger'];
const order = ['info', 'warn', 'success', 'error'];

const msgs = Array.from(this._messages.values())
.sort((a, b) => order.indexOf(a.type) > order.indexOf(b.type));
Expand Down Expand Up @@ -172,7 +181,6 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
};

StockExitForm.prototype.listAvailableInventory = function listAvailableInventory() {
// console.log('#listAvailableInventory():', this._pool.list());
return util.getUniqueBy(this._pool.list(), 'inventory_uuid');
};

Expand All @@ -199,7 +207,7 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
this.details.date = date;

const isPastDate = this.details.date < today;
toggleInfoMessage(isPastDate, WARN_PAST_DATE, this.details);
this._toggleInfoMessage(isPastDate, WARN_PAST_DATE, this.details);

return this.fetchQuantityInStock(this.details.depot_uuid, this.details.date);
};
Expand All @@ -219,7 +227,18 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
*/
StockExitForm.prototype.setExitType = function setExitType(type) {
this.details.exit_type = type;
toggleInfoMessage(false, 'info', INFO_NO_EXIT_TYPE, this.details);
this._toggleInfoMessage(false, 'info', INFO_NO_EXIT_TYPE, this.details);

// clear any previous values set by an exit type
// NOTE(@jniles) - this means that we _must_ call this before setting the other exit types
delete this.details.invoice_uuid;
delete this.details.stock_requisition_uuid;
delete this.details.entity_uuid;

// reset store by releasing all locks on items
// and clearing the data
this.store.data.forEach(item => this.pool.release(item.lot_uuid));
this.store.clear();
};

StockExitForm.prototype.setLotsFromInventoryList = function setLotsFromInventoryList(inventories, uuidKey = 'uuid') {
Expand Down Expand Up @@ -247,7 +266,7 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
});

const hasNoConsumableItems = (available.length === 0 && unavailable.length === 0);
toggleInfoMessage(hasNoConsumableItems, 'warn', WARN_NOT_CONSUMABLE_INVOICE, { ...this.details, inventories });
this._toggleInfoMessage(hasNoConsumableItems, 'warn', WARN_NOT_CONSUMABLE_INVOICE, { ...this.details, inventories });

// if there are no consumable items in the invoice, this will exit early
if (hasNoConsumableItems) {
Expand Down Expand Up @@ -302,9 +321,9 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
});

// finally, toggle compute the error codes
toggleInfoMessage(unavailable > 0, 'warn', WARN_OUT_OF_STOCK_QUANTITY, { ...this.details, unavailable });
toggleInfoMessage(insufficient > 0, 'warn', WARN_INSUFFICIENT_QUANTITY, { ...this.details, insufficient });
toggleInfoMessage(available > 0, 'success', SUCCESS_FILLED_N_ITEMS, { ...this.details, available });
this._toggleInfoMessage(unavailable > 0, 'warn', WARN_OUT_OF_STOCK_QUANTITY, { ...this.details, unavailable });
this._toggleInfoMessage(insufficient > 0, 'warn', WARN_INSUFFICIENT_QUANTITY, { ...this.details, insufficient });
this._toggleInfoMessage(available > 0, 'success', SUCCESS_FILLED_N_ITEMS, { ...this.details, available });
};

/**
Expand Down Expand Up @@ -336,6 +355,10 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
this.details.flux_id = TO_SERVICE;
console.log('service:', service);
console.log('requisition:', service.requisition);

if (service.requisition) {
this.details.stock_requisition_uuid = service.requisition.uuid;
}
};

/**
Expand All @@ -349,6 +372,10 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
this.details.flux_id = TO_OTHER_DEPOT;
console.log('depot:', depot);
console.log('requisition:', depot.requisition);

if (depot.requisition) {
this.details.stock_requisition_uuid = depot.requisition.uuid;
}
};

/**
Expand Down Expand Up @@ -479,25 +506,6 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
return Object.keys(this.cache).length > 0;
};

/**
* @function errorLineHighlight
*
* @description
* Sets the grid's error flag on the row to render a red highlight
* on the row.
*
*/
function errorLineHighlight(rowIdx, store) {
const { ROW_ERROR_FLAG } = bhConstants.grid;
// set and unset error flag for allowing to highlight again the row
// when the user click again on the submit button
const row = store.data[rowIdx];
row[ROW_ERROR_FLAG] = true;
$timeout(() => {
row[ROW_ERROR_FLAG] = false;
}, 1000);
}

/**
* @method validate
*
Expand All @@ -520,20 +528,48 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
// these two conditions require special logic
const hasNoDestination = !this.details.entity_uuid;

// to ensure we validate the entire grid, we must do a pass first, then
// check Array.prototype.every()
const validation = this.store.data.map(lot => lot.validate(this.details.date, !this._isStockLoss()));

// check for valid lots
const hasValidLots = this.store.data.length > 0
&& this.store.data.every(lot => lot.validate(this.details.date, !this._isStockLoss()));
&& validation.every(row => row);

// gather errors into a flat array
this._errors = this.store.data
.flatMap(row => row.errors())
.filter(err => err);

// indicate that the grid has errors in it
this._toggleInfoMessage(this._errors.length, 'error', ERR_LOT_ERRORS, this._errors);

// some exit types require a destination (patient, service, depot).
const hasDestinationError = !this._isStockLoss() && hasNoDestination;
toggleInfoMessage(hasDestinationError, 'danger', ERR_NO_DESTINATION, this.details);
const hasDestinationError = this.details.exit_type && !this._isStockLoss() && hasNoDestination;
this._toggleInfoMessage(hasDestinationError, 'error', ERR_NO_DESTINATION, this.details);

// display a message telling the user to add lots next
const showNeedsLotsInfoMessage = this.details.exit_type
&& !hasDestinationError
&& this.store.data.length === 0;
this._toggleInfoMessage(showNeedsLotsInfoMessage, 'info', INFO_NEEDS_LOTS, this.details);

return hasRequiredDetails
&& hasValidLots
&& !hasDestinationError;
};

return hasRequiredDetails && hasValidLots && !hasDestinationError;
/**
* @function submit
*
* @description
* Submits the values to the server.
*/
StockExitForm.prototype.submit = function submit() {
return this.getDataForSubmission()
.then(data => {
return Stock.movements.create(data);
});
};

/**
Expand All @@ -549,7 +585,16 @@ function StockExitFormService(Store, AppCache, Session, $timeout, bhConstants, D
.then(description => {
Object.assign(data, { description });

data.lots = this.store.data;
// format the lots for submission
data.lots = this.store.data
.map(lot => ({
uuid : lot.lot_uuid,
inventory_uuid : lot.inventory_uuid,
quantity : lot.quantity,
}));

console.log('data:', data);

return data;
});
};
Expand Down
46 changes: 28 additions & 18 deletions client/src/modules/stock/exit/exit.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,6 @@
on-change="StockCtrl.stockForm.setDate(date)">
</bh-date-editor>

<div ng-if="StockCtrl.stockForm.details.date < StockCtrl.today" class="form-group">
<p class="form-control-static text-warning">
<i class="fa fa-warning"></i>
<span translate>STOCK.ANTERIOR_DATE_QUANTITY_MESSAGE</span>
</p>
</div>

<!-- note -->
<div class="form-group"
ng-class="{ 'has-error' : StockExitForm.$submitted && StockExitForm.description.$invalid }">
Expand All @@ -75,18 +68,35 @@
</div>

<!-- message panel to display actions the user needs to take -->
<div class="form-group">
<label translate>Pending Actions</label>
<p translate class="form-control-static" ng-class="{
'text-danger' : StockCtrl.message.type === 'error',
'text-primary' : StockCtrl.message.type === 'info',
'text-success' : StockCtrl.message.type === 'success',
<div class="form-group" style="min-height: 15em;">
<label class="control-label" translate>STOCK.PENDING_ACTIONS</label>

<p
style="padding-top:0"
class="form-control-static"
ng-repeat="message in StockCtrl.messages"
ng-class="{
'text-danger' : message.type === 'error',
'text-warning' : message.type === 'warn',
'text-primary' : message.type === 'info',
'text-success' : message.type === 'success',
}">
{{ StockCtrl.message.text }}
<i class="fa"
ng-class="{
'fa-exclamation-triangle' : message.type === 'error',
'fa-exclamation-circle' : message.type === 'warn',
'fa-chevron-circle-right' : message.type === 'info',
'fa-check-circle-o' : message.type === 'success',
}">
</i>
<span translate>{{ message.text }}<span>
</p>
</div>

<p ng-if="StockCtrl.messages.length === 0" style="padding-top:0" class="form-control-static text-success">
<i class="fa fa-check-circle-o"></i> <span translate>STOCK.MESSAGES.SUCCESS_ALL_VALID<span>
</p>

</div>
</div>

<div class="col-xs-6 col-md-3 panel-default">
Expand All @@ -109,7 +119,7 @@
<div class="grid-toolbar-item">
<bh-add-item
disable="StockCtrl.stockForm.details.exit_type"
callback="StockCtrl.stockForm.addItems(numItem)">
callback="StockCtrl.addItems(numItem)">
</bh-add-item>
</div>

Expand Down Expand Up @@ -156,8 +166,8 @@
</div>

<!-- footer -->
<div class="row" style="margin-top: 5px;">
<div class="col-md-6 text-right">
<div class="row" style="margin-top: 5px; margin-bottom:5px;">
<div class="col-md-12 text-right">
<button class="btn btn-default" ng-click="StockCtrl.stockForm.clear()" type="button" translate>
FORM.BUTTONS.CLEAR
</button>
Expand Down

0 comments on commit 4f96dfe

Please sign in to comment.