Skip to content

Commit

Permalink
Merge #5143
Browse files Browse the repository at this point in the history
5143: Fix stock performance issues r=jniles a=jniles

This PR addresses two performance issues:
 1. We cache the results of the stock out/expired stock components on the depot page to prevent multiple calls to that route from hitting the server simultaneously.
 2. We've removed calls to `stock_consumption` in the stock controller and merged the calls to `getCMM` into a single function. Then we optimized the function to only call `getCMM()` for unique depot/inventory pairs.

While doing so, I've refactored the CMM calculations in stock/core.js to make the more streamlined and rename generic sounding names to more meaningful names .  There are two basic paths through core.js:

1. `getBulkInventoryCMM()` -> `computeInventoryIndicators()`
2. `getBulkInventoryCMM()` -> `computeInventoryIndicators()` -> `computeLotIndicators()`

The first path computes S_MAX, S_MIN, and other _inventory_ indicators that relate to the change of stock out.  This is good enough for the "Items in Stock" registry and other reports that need to see the data on an inventory level.

The second path does everything in the first, but also computes the S_RISK and S_RISK_QUANTITY and other _lot_ indicators at are related to the risk of expiration of an individual lot.

Closes #4711
Partially addresses #5127
Partially addresses #5073

Co-authored-by: Jonathan Niles <jonathanwniles@gmail.com>
  • Loading branch information
bors[bot] and jniles authored Dec 2, 2020
2 parents e2f8bd0 + f35575a commit d104d9e
Show file tree
Hide file tree
Showing 16 changed files with 214 additions and 296 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<th translate>FORM.LABELS.LOT</th>
</tr>
<tr ng-repeat="inventory in $ctrl.expiredInventories | limitTo : 5">
<td>{{inventory.text}} (<u translate-attr="{'title' : inventory.expiration_date_raw}">{{inventory.expiration_date}}</u>)</td>
<td>{{inventory.text}} (<u translate-attr="{'title' : inventory.expiration_date_raw}">{{inventory.expiration_date_parsed}}</u>)</td>
<td class="lot-width">{{inventory.label}}</td>
</tr>

Expand Down
2 changes: 1 addition & 1 deletion client/src/js/components/bhStockExpired/bhStockExpired.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function bhStockExpiredController(Stock, moment, Notify, Depot, $filter) {
.then(inventories => {
inventories.forEach(inventory => {
inventory.expiration_date_raw = $date(inventory.expiration_date);
inventory.expiration_date = moment(inventory.expiration_date).fromNow();
inventory.expiration_date_parsed = moment(inventory.expiration_date).fromNow();
});

$ctrl.expiredInventories = inventories;
Expand Down
12 changes: 6 additions & 6 deletions client/src/js/components/bhStockSoldOut/bhStockSoldOut.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@

<table
class="table table-condensed table-bordered"
ng-show="$ctrl.soldOutInventories.length > 0">
ng-show="$ctrl.stockOutInventories.length > 0">
<thead>
<tr>
<th>
<span class="text-danger"
translate-values="{ total : $ctrl.soldOutInventories.length }"
translate-values="{ total : $ctrl.stockOutInventories.length }"
translate="{{ 'STOCK.STOCK_OUT_WARNING' }}">
</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat = "inventory in $ctrl.soldOutInventories | limitTo : 5">
<td>{{inventory.text}} (<u translate-attr="{ 'title' : inventory.stock_out_date_raw }">{{inventory.stock_out_date}}</u>)</td>
<tr ng-repeat = "inventory in $ctrl.stockOutInventories | limitTo : 5">
<td>{{inventory.text}} (<u translate-attr="{ 'title' : inventory.stock_out_date_raw }">{{inventory.stock_out_date_parsed}}</u>)</td>
</tr>

<tr>
<td>
<span class="text-danger"
ng-show="($ctrl.soldOutInventories.length - 5) > 0"
translate-values="{ left : ($ctrl.soldOutInventories.length - 5)}"
ng-show="($ctrl.stockOutInventories.length - 5) > 0"
translate-values="{ left : ($ctrl.stockOutInventories.length - 5)}"
translate="{{ 'STOCK.AND_MORE' }}">
</span>

Expand Down
7 changes: 4 additions & 3 deletions client/src/js/components/bhStockSoldOut/bhStockSoldOut.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ bhStockSoldOutController.$inject = ['StockService', 'moment', 'NotifyService', '
function bhStockSoldOutController(Stock, moment, Notify, $filter) {
const $ctrl = this;
$ctrl.loading = false;
$ctrl.soldOutInventories = [];
$ctrl.stockOutInventories = [];

const $date = $filter('date');

Expand All @@ -34,6 +34,7 @@ function bhStockSoldOutController(Stock, moment, Notify, $filter) {
*/
function fetchStockOuts() {
if (!$ctrl.depotUuid) return;

const dateTo = $ctrl.date || new Date();
$ctrl.loading = true;

Expand All @@ -45,10 +46,10 @@ function bhStockSoldOutController(Stock, moment, Notify, $filter) {
.then(inventories => {
inventories.forEach(inventory => {
inventory.stock_out_date_raw = $date(inventory.stock_out_date);
inventory.stock_out_date = moment(inventory.stock_out_date).fromNow();
inventory.stock_out_date_parsed = moment(inventory.stock_out_date).fromNow();
});

$ctrl.soldOutInventories = inventories;
$ctrl.stockOutInventories = inventories;
})
.catch(Notify.handleError)
.finally(() => {
Expand Down
26 changes: 24 additions & 2 deletions client/src/js/services/StockService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ angular.module('bhima.services')
.service('StockService', StockService);

StockService.$inject = [
'PrototypeApiService', 'StockFilterer',
'PrototypeApiService', 'StockFilterer', 'HttpCacheService',
];

function StockService(Api, StockFilterer) {
function StockService(Api, StockFilterer, HttpCache) {
// API for stock lots
const stocks = new Api('/stock/lots');

Expand All @@ -23,6 +23,28 @@ function StockService(Api, StockFilterer) {
// API for stock inventory in depots
const inventories = new Api('/stock/inventories/depots');

// the stock inventories route gets hit a lot. Cache the results on the client.
inventories.read = cacheInventoriesRead;

const callback = (uuid, options) => Api.read.call(inventories, uuid, options);
const fetcher = HttpCache(callback, 5000);

/**
* The read() method loads data from the api endpoint. If an id is provided,
* the $http promise is resolved with a single JSON object, otherwise an array
* of objects should be expected.
*
* @param {String} uuid - the uuid of the inventory to fetch (optional).
* @param {Object} options - options to be passed as query strings (optional).
* @param {Boolean} cacheBust - ignore the cache and send the HTTP request directly
* to the server.
* @return {Promise} promise - resolves to either a JSON (if id provided) or
* an array of JSONs.
*/
function cacheInventoriesRead(uuid, options, cacheBust = false) {
return fetcher(uuid, options, cacheBust);
}

// API for stock inventory adjustment
const inventoryAdjustment = new Api('/stock/inventory_adjustment');

Expand Down
4 changes: 3 additions & 1 deletion client/src/modules/accounts/accounts.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ AccountService.$inject = [
];

/**
* Account Service
* @class AccountService
* @extends PrototypeApiService
*
* @description
* A service wrapper for the /accounts HTTP endpoint.
*/
function AccountService(Api, bhConstants, HttpCache) {
Expand Down
30 changes: 25 additions & 5 deletions client/src/modules/depots/depots.service.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
angular.module('bhima.services')
.service('DepotService', DepotService);

DepotService.$inject = ['PrototypeApiService', '$uibModal'];
DepotService.$inject = ['PrototypeApiService', '$uibModal', 'HttpCacheService'];

/**
* @class DepotService
Expand All @@ -10,10 +10,32 @@ DepotService.$inject = ['PrototypeApiService', '$uibModal'];
* @description
* Encapsulates common requests to the /depots/ URL.
*/
function DepotService(Api, Modal) {
function DepotService(Api, Modal, HttpCache) {
const baseUrl = '/depots/';
const service = new Api(baseUrl);

// debounce the read() call for depots
service.read = read;

const callback = (id, options) => Api.read.call(service, id, options);
const fetcher = HttpCache(callback, 250);

/**
* The read() method loads data from the api endpoint. If an id is provided,
* the $http promise is resolved with a single JSON object, otherwise an array
* of objects should be expected.
*
* @param {String} uuid - the uuid of the depot to fetch (optional).
* @param {Object} options - options to be passed as query strings (optional).
* @param {Boolean} cacheBust - ignore the cache and send the HTTP request directly
* to the server.
* @return {Promise} promise - resolves to either a JSON (if id provided) or
* an array of JSONs.
*/
function read(uuid, options, cacheBust = false) {
return fetcher(uuid, options, cacheBust);
}

/**
* @method openSelectionModal
*
Expand All @@ -29,9 +51,7 @@ function DepotService(Api, Modal) {
return Modal.open({
controller : 'SelectDepotModalController as $ctrl',
templateUrl : 'modules/stock/depot-selection.modal.html',
resolve : {
depot : () => depot,
},
resolve : { depot : () => depot },
}).result;
};

Expand Down
1 change: 0 additions & 1 deletion client/src/modules/stock/lots/registry.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ function LotsRegistryService(uiGridConstants, Session) {
delete lot.expiration_date;
delete lot.lifetime;
delete lot.S_LOT_LIFETIME;
delete lot.S_RP;
}
};

Expand Down
5 changes: 4 additions & 1 deletion server/controllers/inventory/depots/extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ router.get('/inventories/:inventoryUuid/lots', getInventoryLots);
async function getInventory(req, res, next) {
try {
const monthAvgConsumption = req.session.stock_settings.month_average_consumption;
const averageConsumptionAlgo = req.session.stock_settings.average_consumption_algo;
const inventory = await core.getInventoryQuantityAndConsumption(
{ depot_uuid : req.params.uuid },
monthAvgConsumption,
averageConsumptionAlgo,
);

res.status(200).json(inventory);
Expand Down Expand Up @@ -97,7 +99,8 @@ async function getInventoryAverageMonthlyConsumption(req, res, next) {

async function getInventoryLots(req, res, next) {
try {
const inventory = await core.getLotsDepot(req.params.uuid, { inventory_uuid : req.params.inventoryUuid });
const options = { inventory_uuid : req.params.inventoryUuid, ...req.session.stock_settings };
const inventory = await core.getLotsDepot(req.params.uuid, options);
res.status(200).json(inventory);
} catch (err) {
next(err);
Expand Down
Loading

0 comments on commit d104d9e

Please sign in to comment.