From 599278c6898e54aca0cbf67ca3527e8e5b4b7a5c Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Tue, 24 Nov 2020 09:46:03 +0100 Subject: [PATCH 1/4] perf(stock): cache inventory read()s Caches the results for the stock components to ensure that we do not overwhelm our servers with HTTP requests. --- .../bhStockExpired/bhStockExpired.html | 2 +- .../bhStockExpired/bhStockExpired.js | 2 +- .../bhStockSoldOut/bhStockSoldOut.html | 12 ++++---- .../bhStockSoldOut/bhStockSoldOut.js | 7 +++-- client/src/js/services/StockService.js | 26 ++++++++++++++-- .../src/modules/accounts/accounts.service.js | 4 ++- client/src/modules/depots/depots.service.js | 30 +++++++++++++++---- 7 files changed, 64 insertions(+), 19 deletions(-) diff --git a/client/src/js/components/bhStockExpired/bhStockExpired.html b/client/src/js/components/bhStockExpired/bhStockExpired.html index 8a82273c22..4756526524 100644 --- a/client/src/js/components/bhStockExpired/bhStockExpired.html +++ b/client/src/js/components/bhStockExpired/bhStockExpired.html @@ -21,7 +21,7 @@ FORM.LABELS.LOT - {{inventory.text}} ({{inventory.expiration_date}}) + {{inventory.text}} ({{inventory.expiration_date_parsed}}) {{inventory.label}} diff --git a/client/src/js/components/bhStockExpired/bhStockExpired.js b/client/src/js/components/bhStockExpired/bhStockExpired.js index 11b2ad270f..6676680136 100644 --- a/client/src/js/components/bhStockExpired/bhStockExpired.js +++ b/client/src/js/components/bhStockExpired/bhStockExpired.js @@ -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; diff --git a/client/src/js/components/bhStockSoldOut/bhStockSoldOut.html b/client/src/js/components/bhStockSoldOut/bhStockSoldOut.html index 4601ec11d4..147fceff80 100644 --- a/client/src/js/components/bhStockSoldOut/bhStockSoldOut.html +++ b/client/src/js/components/bhStockSoldOut/bhStockSoldOut.html @@ -4,27 +4,27 @@ + ng-show="$ctrl.stockOutInventories.length > 0"> - - + +
{{inventory.text}} ({{inventory.stock_out_date}})
{{inventory.text}} ({{inventory.stock_out_date_parsed}})
diff --git a/client/src/js/components/bhStockSoldOut/bhStockSoldOut.js b/client/src/js/components/bhStockSoldOut/bhStockSoldOut.js index 2ce2fa2c5d..6523c7dbac 100644 --- a/client/src/js/components/bhStockSoldOut/bhStockSoldOut.js +++ b/client/src/js/components/bhStockSoldOut/bhStockSoldOut.js @@ -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'); @@ -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; @@ -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(() => { diff --git a/client/src/js/services/StockService.js b/client/src/js/services/StockService.js index 95301745a0..321e529050 100644 --- a/client/src/js/services/StockService.js +++ b/client/src/js/services/StockService.js @@ -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'); @@ -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 = (id, options) => Api.read.call(inventories, id, 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 {Number} id - the id of the account 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(id, options, cacheBust = false) { + return fetcher(id, options, cacheBust); + } + // API for stock inventory adjustment const inventoryAdjustment = new Api('/stock/inventory_adjustment'); diff --git a/client/src/modules/accounts/accounts.service.js b/client/src/modules/accounts/accounts.service.js index b769c5caa4..a5c51020a0 100644 --- a/client/src/modules/accounts/accounts.service.js +++ b/client/src/modules/accounts/accounts.service.js @@ -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) { diff --git a/client/src/modules/depots/depots.service.js b/client/src/modules/depots/depots.service.js index 8646af8e5a..d94a04b0f0 100644 --- a/client/src/modules/depots/depots.service.js +++ b/client/src/modules/depots/depots.service.js @@ -1,7 +1,7 @@ angular.module('bhima.services') .service('DepotService', DepotService); -DepotService.$inject = ['PrototypeApiService', '$uibModal']; +DepotService.$inject = ['PrototypeApiService', '$uibModal', 'HttpCacheService']; /** * @class DepotService @@ -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 {Number} id - the id of the account 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(id, options, cacheBust = false) { + return fetcher(id, options, cacheBust); + } + /** * @method openSelectionModal * @@ -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; }; From 9353fdf7cb254da0dd039fb2770355139ba3ec9c Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Tue, 24 Nov 2020 20:42:42 +0100 Subject: [PATCH 2/4] perf(stock): refactor CMM calculations Removes extraneous code paths from the CMM calculation and combines the duplicated code into a single function to calculate the CMM. Also remove the redundant calls to getCMM(). --- client/src/js/services/StockService.js | 8 +- client/src/modules/depots/depots.service.js | 6 +- server/controllers/stock/core.js | 246 +++++--------------- server/models/procedures/stock.sql | 10 +- 4 files changed, 71 insertions(+), 199 deletions(-) diff --git a/client/src/js/services/StockService.js b/client/src/js/services/StockService.js index 321e529050..28c9947193 100644 --- a/client/src/js/services/StockService.js +++ b/client/src/js/services/StockService.js @@ -26,7 +26,7 @@ function StockService(Api, StockFilterer, HttpCache) { // the stock inventories route gets hit a lot. Cache the results on the client. inventories.read = cacheInventoriesRead; - const callback = (id, options) => Api.read.call(inventories, id, options); + const callback = (uuid, options) => Api.read.call(inventories, uuid, options); const fetcher = HttpCache(callback, 5000); /** @@ -34,15 +34,15 @@ function StockService(Api, StockFilterer, HttpCache) { * the $http promise is resolved with a single JSON object, otherwise an array * of objects should be expected. * - * @param {Number} id - the id of the account to fetch (optional). + * @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(id, options, cacheBust = false) { - return fetcher(id, options, cacheBust); + function cacheInventoriesRead(uuid, options, cacheBust = false) { + return fetcher(uuid, options, cacheBust); } // API for stock inventory adjustment diff --git a/client/src/modules/depots/depots.service.js b/client/src/modules/depots/depots.service.js index d94a04b0f0..fb8fd33b5b 100644 --- a/client/src/modules/depots/depots.service.js +++ b/client/src/modules/depots/depots.service.js @@ -25,15 +25,15 @@ function DepotService(Api, Modal, HttpCache) { * the $http promise is resolved with a single JSON object, otherwise an array * of objects should be expected. * - * @param {Number} id - the id of the account to fetch (optional). + * @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(id, options, cacheBust = false) { - return fetcher(id, options, cacheBust); + function read(uuid, options, cacheBust = false) { + return fetcher(uuid, options, cacheBust); } /** diff --git a/server/controllers/stock/core.js b/server/controllers/stock/core.js index c10744dfd7..cfc99cf378 100644 --- a/server/controllers/stock/core.js +++ b/server/controllers/stock/core.js @@ -35,8 +35,6 @@ const flux = { INVENTORY_ADJUSTMENT : 15, }; -const DATE_FORMAT = 'YYYY-MM-DD'; - // exports module.exports = { flux, @@ -45,11 +43,8 @@ module.exports = { getLotsDepot, getLotsMovements, getLotsOrigins, - stockManagementProcess, listStatus, // stock consumption - getStockConsumption, - getStockConsumptionAverage, getInventoryQuantityAndConsumption, getInventoryMovements, getDailyStockConsumption, @@ -264,37 +259,12 @@ async function getLotsDepot(depotUuid, params, finalClause) { const query = filters.applyQuery(sql); const queryParameters = filters.parameters(); - const inventories = await db.exec(query, queryParameters); - const processParameters = [inventories, params.dateTo, params.monthAverageConsumption]; - const resultFromProcess = await processStockConsumptionAverage(...processParameters); - - if (resultFromProcess.length > 0) { - let startDate = new Date(); - let clone = moment(startDate); - const months = (params.monthAverageConsumption - 1 > -1 && params.monthAverageConsumption) - ? params.monthAverageConsumption - 1 : 0; - clone = clone.subtract(months, 'month').toDate(); - - startDate = new Date(clone); - const endDate = new Date(); - - const cmms = await Promise.all(resultFromProcess.map(inventory => { - return db.exec(`CALL getCMM(?,?,?,?) `, [ - startDate, - endDate, - db.bid(inventory.inventory_uuid), - db.bid(inventory.depot_uuid), - ]); - })); - - resultFromProcess.forEach((inv, index) => { - const cmmResult = cmms[index][0][0]; - inv.cmms = cmmResult; - inv.avg_consumption = cmmResult[params.averageConsumptionAlgo]; - }); - } + const resultFromProcess = await db.exec(query, queryParameters); - const inventoriesWithManagementData = await stockManagementProcess(resultFromProcess); + // calulate the CMM for a whole series of + await getBulkInventoryCMM(resultFromProcess, params.monthAverageConsumption, params.averageConsumptionAlgo); + + const inventoriesWithManagementData = computeInventoryIndicators(resultFromProcess); let inventoriesWithLotsProcessed = await processMultipleLots(inventoriesWithManagementData); if (_status) { @@ -315,6 +285,49 @@ async function getLotsDepot(depotUuid, params, finalClause) { return inventoriesWithLotsProcessed; } +/** + * @function getBulkInventoryCMM + * + * @description + * Gets the bulk CMM for the inventory items. + */ +async function getBulkInventoryCMM(lots, monthAverageConsumption, averageConsumptionAlgo) { + if (!lots.length) return []; + + const months = (monthAverageConsumption - 1 > -1 && monthAverageConsumption) + ? monthAverageConsumption - 1 : 0; + + const startDate = moment().subtract(months, 'month').toDate(); + const endDate = new Date(); + + // create a list of unique depot/inventory_uuid maps to avoid querying the server multiple + // times. + const params = _.chain(lots) + .map(row => [startDate, endDate, row.inventory_uuid, row.depot_uuid]) + .uniqBy(_.isEqual) + .value(); + + // collect the current cmm for the following inventory items. + const cmms = await Promise.all( + params.map(row => db.exec(`CALL getCMM(?,?, HUID(?), HUID(?))`, row).then(values => values[0][0])), + ); + + const inventoryMap = _.groupBy(cmms, 'inventory_uuid'); + + lots.forEach(lot => { + const hasConsumption = inventoryMap[lot.inventory_uuid]; + if (hasConsumption) { + [lot.cmms] = hasConsumption; + lot.avg_consumption = lot.cmms[averageConsumptionAlgo]; + } else { + lot.cmms = {}; + lot.avg_consumption = 0; + } + }); + + return lots; +} + /** * @function getLotsMovements * @@ -446,7 +459,7 @@ function getLotsOrigins(depotUuid, params, averageConsumptionAlgo) { } /** - * @function stockManagementProcess + * @function computeInventoryIndicators * * @description * Stock Management Processing @@ -456,7 +469,7 @@ function getLotsOrigins(depotUuid, params, averageConsumptionAlgo) { * S_MIN: Minimum stock - typically the security stock (depends on the depot) * S_RP: Risk of Expiration. */ -function stockManagementProcess(inventories) { +function computeInventoryIndicators(inventories) { let CM; let Q; let CM_NOT_ZERO; @@ -504,6 +517,8 @@ function stockManagementProcess(inventories) { // can use them. inventory.S_RP = inventory.quantity - (inventory.lifetime * CM); // risque peremption + console.log('inventory:', inventory); + // compute the inventory status code if (Q <= 0) { inventory.status = 'stock_out'; @@ -529,26 +544,6 @@ function stockManagementProcess(inventories) { return inventories; } -/** - * @function getStockConsumption - * - * @description returns the monthly (periodic) stock consumption (CM) - * - * @param {array} periodIds - */ -function getStockConsumption(periodIds) { - const sql = ` - SELECT SUM(s.quantity) AS quantity, BUID(i.uuid) AS uuid, i.text, i.code, d.text - FROM stock_consumption s - JOIN inventory i ON i.uuid = s.inventory_uuid - JOIN depot d ON d.uuid = s.depot_uuid - JOIN period p ON p.id = s.period_id - WHERE p.id IN (?) - GROUP BY i.uuid, d.uuid - `; - return db.exec(sql, [periodIds]); -} - /** * @function getDailyStockConsumption * @@ -611,73 +606,6 @@ async function getDailyStockConsumption(params) { return db.exec(rqtSQl, rqtParams); } -/** - * @function getStockConsumptionAverage - * - * @description - * Algorithm to calculate the CMM (consommation moyenne mensuelle) or average stock consumption - * over a period for each stock item that has been consumed. - * - * NOTE: A FISCAL YEAR MUST BE DEFINED FOR THE FEATURE WORK PROPERLY - * - * @param {number} periodId - the base period - * @param {Date} periodDate - a date for finding the correspondant period - * @param {number} monthAverageConsumption - the number of months for calculating the average (optional) - */ -async function getStockConsumptionAverage(periodId, periodDate, monthAverageConsumption) { - const numberOfMonths = monthAverageConsumption - 1; - let ids = []; - - const baseDate = periodDate - ? moment(periodDate).format(DATE_FORMAT) - : moment().format(DATE_FORMAT); - - const beginningDate = moment(baseDate) - .subtract(numberOfMonths, 'months') - .format(DATE_FORMAT); - - const queryPeriodRange = ` - SELECT id FROM period WHERE (id BETWEEN ? AND ?) AND period.number NOT IN (0, 13); - `; - - const queryPeriodId = periodId - ? 'SELECT id FROM period WHERE id = ? LIMIT 1;' - : 'SELECT id FROM period WHERE DATE(?) BETWEEN DATE(start_date) AND DATE(end_date) LIMIT 1;'; - - const queryStockConsumption = ` - SELECT IF(i.avg_consumption = 0, ROUND(AVG(s.quantity)), i.avg_consumption) AS quantity, - BUID(i.uuid) AS uuid, i.text, i.code, BUID(d.uuid) AS depot_uuid, - d.text AS depot_text - FROM stock_consumption s - JOIN inventory i ON i.uuid = s.inventory_uuid - JOIN depot d ON d.uuid = s.depot_uuid - JOIN period p ON p.id = s.period_id - WHERE p.id IN (?) - GROUP BY i.uuid, d.uuid; - `; - - const checkPeriod = 'SELECT id FROM period;'; - - const getBeginningPeriod = ` - SELECT id FROM period WHERE DATE(?) BETWEEN DATE(start_date) AND DATE(end_date) LIMIT 1; - `; - - const periods = await db.exec(checkPeriod); - - // Just to avoid that db.one requests can query empty tables, and generate errors - if (periods.length) { - const period = await db.one(queryPeriodId, [periodId || baseDate]); - const beginningPeriod = await db.one(getBeginningPeriod, [beginningDate]); - const paramPeriodRange = beginningPeriod.id ? [beginningPeriod.id, period.id] : [1, period.id]; - const rows = await db.exec(queryPeriodRange, paramPeriodRange); - ids = rows.map(row => row.id); - } - - const execStockConsumption = periods.length ? db.exec(queryStockConsumption, [ids]) : []; - - return execStockConsumption; -} - /** * Inventory Quantity and Consumptions */ @@ -739,41 +667,15 @@ async function getInventoryQuantityAndConsumption(params, monthAverageConsumptio const clause = ` GROUP BY l.inventory_uuid, m.depot_uuid ${emptyLotToken} ORDER BY ig.name, i.text `; - const inventories = await getLots(sql, params, clause); - const processParams = [inventories, params.dateTo, monthAverageConsumption]; - const inventoriesProcessed = await processStockConsumptionAverage(...processParams); - - let filteredRows = inventoriesProcessed; - - if (filteredRows.length > 0) { - const settingsql = `SELECT month_average_consumption FROM stock_setting WHERE enterprise_id=?`; - const setting = await db.one(settingsql, filteredRows[0].enterprise_id); - const nbrMonth = setting.month_average_consumption; - - let startDate = new Date(); - let clone = moment(startDate); - const months = (nbrMonth - 1 > -1 && nbrMonth) ? nbrMonth - 1 : 0; - clone = clone.subtract(months, 'month').toDate(); - - startDate = new Date(clone); - const endDate = new Date(); - - const cmms = await Promise.all(filteredRows.map(inventory => { - return db.exec(`CALL getCMM(?,?,?,?) `, [ - startDate, - endDate, - db.bid(inventory.inventory_uuid), - db.bid(inventory.depot_uuid), - ]); - })); - filteredRows.forEach((inv, index) => { - const cmmResult = cmms[index][0][0]; - inv.cmms = cmmResult; - inv.avg_consumption = cmmResult[averageConsumptionAlgo]; - }); - } + let filteredRows = await getLots(sql, params, clause); + + const settingsql = `SELECT month_average_consumption FROM stock_setting WHERE enterprise_id = ?`; + const setting = await db.one(settingsql, filteredRows[0].enterprise_id); + + // add the CMM + await getBulkInventoryCMM(filteredRows, setting.month_average_consumption, averageConsumptionAlgo); - filteredRows = await stockManagementProcess(filteredRows, delay, purchaseInterval); + filteredRows = computeInventoryIndicators(filteredRows, delay, purchaseInterval); if (_status) { filteredRows = filteredRows.filter(row => row.status === _status); @@ -849,34 +751,6 @@ function processMultipleLots(inventories) { return flattenLots; } -/** - * @function processStockConsumptionAverage - * - * @description - * This function reads the average stock consumption for each inventory item - * in a depot. - */ -async function processStockConsumptionAverage( - inventories, dateTo, monthAverageConsumption, -) { - const consumptions = await getStockConsumptionAverage( - null, dateTo, monthAverageConsumption, - ); - - for (let i = 0; i < consumptions.length; i++) { - for (let j = 0; j < inventories.length; j++) { - const isSameInventory = consumptions[i].uuid === inventories[j].inventory_uuid; - const isSameDepot = consumptions[i].depot_uuid === inventories[j].depot_uuid; - if (isSameInventory && isSameDepot) { - inventories[j].avg_consumption = consumptions[i].quantity; - break; - } - } - } - - return inventories; -} - /** * Inventory Movement Report */ diff --git a/server/models/procedures/stock.sql b/server/models/procedures/stock.sql index 8028f0613b..997e7422f6 100644 --- a/server/models/procedures/stock.sql +++ b/server/models/procedures/stock.sql @@ -282,10 +282,10 @@ BEGIN END IF; /* continue only if inventoryUuid is defined */ - IF (inventoryUuid IS NOT NULL) THEN + IF (inventoryUuid IS NOT NULL) THEN /* update the consumption (avg_consumption) */ - IF (inventoryCmm IS NOT NULL OR inventoryCmm <> '' OR inventoryCmm <> 'NULL') THEN + IF (inventoryCmm IS NOT NULL OR inventoryCmm <> '' OR inventoryCmm <> 'NULL') THEN UPDATE inventory SET avg_consumption = inventoryCmm WHERE `uuid` = inventoryUuid; END IF; @@ -586,7 +586,7 @@ CREATE PROCEDURE `getCMM` ( IN _depot_uuid BINARY(16) ) BEGIN - DECLARE _last_inventory_mvt_date, _first_inventory_mvt_date DATE; + DECLARE _last_inventory_mvt_date, _first_inventory_mvt_date DATE; DECLARE _sum_consumed_quantity, _sum_stock_day, _sum_consumption_day, _sum_stock_out_days, _sum_days, _number_of_month, _days_before_consumption @@ -621,7 +621,6 @@ CREATE PROCEDURE `getCMM` ( ORDER BY sm.date ASC ) AS aggr; - SET _sum_days = DATEDIFF(_end_date, _start_date) + 1; SET _number_of_month = ROUND(DATEDIFF(_end_date, _start_date)/30.5); SET _days_before_consumption = DATEDIFF(_first_inventory_mvt_date, _start_date); @@ -636,7 +635,6 @@ CREATE PROCEDURE `getCMM` ( x.inventory, x.depot, (DATEDIFF(x.end_date, x.start_date) + 1) AS frequency FROM ( SELECT - IF(m.start_date < _start_date, _start_date, m.start_date) AS start_date , IF(m.end_date > _end_date, _end_date, IF(m.end_date = _last_inventory_mvt_date AND _last_inventory_mvt_date < _end_date, _end_date, m.end_date )) AS end_date, @@ -669,6 +667,7 @@ CREATE PROCEDURE `getCMM` ( ROUND(IFNULL(@algo2, 0), 2) as algo2, ROUND(IFNULL(@algo3, 0),2) as algo3, ROUND(IFNULL(@algo_msh, 0), 2) as algo_msh, + BUID(_inventory_uuid) as inventory_uuid, _start_date as start_date, _end_date as end_date, _first_inventory_mvt_date as first_inventory_movement_date, @@ -680,7 +679,6 @@ CREATE PROCEDURE `getCMM` ( _number_of_month as number_of_month, _sum_stock_out_days as sum_stock_out_days, _days_before_consumption as days_before_consumption; - END$$ DELIMITER ; From abd0a3ca5571f3f0bed6b95ebe9ca4a778ac80eb Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Wed, 25 Nov 2020 08:54:06 +0100 Subject: [PATCH 3/4] chore(stock): remove unused indicators Removed unused indicators from the stock core reporting. --- .../modules/stock/lots/registry.service.js | 1 - server/controllers/inventory/depots/extra.js | 5 +- server/controllers/stock/core.js | 175 +++++++++--------- server/controllers/stock/index.js | 14 +- .../stock/reports/stock/lots_report.js | 5 +- server/models/procedures/stock.sql | 1 + test/data.sql | 2 +- test/end-to-end/stock/stock.inventories.js | 14 +- 8 files changed, 109 insertions(+), 108 deletions(-) diff --git a/client/src/modules/stock/lots/registry.service.js b/client/src/modules/stock/lots/registry.service.js index fe8aed17e9..c108148e4c 100644 --- a/client/src/modules/stock/lots/registry.service.js +++ b/client/src/modules/stock/lots/registry.service.js @@ -186,7 +186,6 @@ function LotsRegistryService(uiGridConstants, Session) { delete lot.expiration_date; delete lot.lifetime; delete lot.S_LOT_LIFETIME; - delete lot.S_RP; } }; diff --git a/server/controllers/inventory/depots/extra.js b/server/controllers/inventory/depots/extra.js index a6031a759b..94e2e54e17 100644 --- a/server/controllers/inventory/depots/extra.js +++ b/server/controllers/inventory/depots/extra.js @@ -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); @@ -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); diff --git a/server/controllers/stock/core.js b/server/controllers/stock/core.js index cfc99cf378..59c607a1eb 100644 --- a/server/controllers/stock/core.js +++ b/server/controllers/stock/core.js @@ -234,21 +234,19 @@ async function getLotsDepot(depotUuid, params, finalClause) { ROUND(DATEDIFF(l.expiration_date, CURRENT_DATE()) / 30.5) AS lifetime, BUID(l.inventory_uuid) AS inventory_uuid, BUID(l.origin_uuid) AS origin_uuid, i.code, i.text, BUID(m.depot_uuid) AS depot_uuid, - m.date AS entry_date, - i.avg_consumption, i.purchase_interval, i.delay, + m.date AS entry_date, i.avg_consumption, i.purchase_interval, i.delay, iu.text AS unit_type, ig.name AS group_name, ig.tracking_expiration, ig.tracking_consumption, - dm.text AS documentReference, - t.name AS tag_name, t.color + dm.text AS documentReference, t.name AS tag_name, t.color FROM stock_movement m - JOIN lot l ON l.uuid = m.lot_uuid - JOIN inventory i ON i.uuid = l.inventory_uuid - JOIN inventory_unit iu ON iu.id = i.unit_id - JOIN inventory_group ig ON ig.uuid = i.group_uuid - JOIN depot d ON d.uuid = m.depot_uuid - LEFT JOIN document_map dm ON dm.uuid = m.document_uuid - LEFT JOIN lot_tag lt ON lt.lot_uuid = l.uuid - LEFT JOIN tags t ON t.uuid = lt.tag_uuid + JOIN lot l ON l.uuid = m.lot_uuid + JOIN inventory i ON i.uuid = l.inventory_uuid + JOIN inventory_unit iu ON iu.id = i.unit_id + JOIN inventory_group ig ON ig.uuid = i.group_uuid + JOIN depot d ON d.uuid = m.depot_uuid + LEFT JOIN document_map dm ON dm.uuid = m.document_uuid + LEFT JOIN lot_tag lt ON lt.lot_uuid = l.uuid + LEFT JOIN tags t ON t.uuid = lt.tag_uuid `; const groupByClause = finalClause || ` GROUP BY l.uuid, m.depot_uuid ${emptyLotToken} ORDER BY i.code, l.label `; @@ -261,11 +259,16 @@ async function getLotsDepot(depotUuid, params, finalClause) { const resultFromProcess = await db.exec(query, queryParameters); - // calulate the CMM for a whole series of - await getBulkInventoryCMM(resultFromProcess, params.monthAverageConsumption, params.averageConsumptionAlgo); + // calulate the CMM and add inventory flags. + const inventoriesWithManagementData = await getBulkInventoryCMM( + resultFromProcess, + params.month_average_consumption, + params.average_consumption_algo, + ); - const inventoriesWithManagementData = computeInventoryIndicators(resultFromProcess); - let inventoriesWithLotsProcessed = await processMultipleLots(inventoriesWithManagementData); + // FIXME(@jniles) - this step seems to mostly just change the ordering of lots. Can we combine + // it with the getBulkInventoryCMM? + let inventoriesWithLotsProcessed = computeLotIndicators(inventoriesWithManagementData); if (_status) { inventoriesWithLotsProcessed = inventoriesWithLotsProcessed.filter(row => row.status === _status); @@ -289,35 +292,42 @@ async function getLotsDepot(depotUuid, params, finalClause) { * @function getBulkInventoryCMM * * @description - * Gets the bulk CMM for the inventory items. + * This function takes in an array of lots or inventory items and computes the CMM for all unique + * inventory/depot pairings in the array. It then creates a mapping for the CMMs in memory and uses + * those to compute the relevant indicators. */ async function getBulkInventoryCMM(lots, monthAverageConsumption, averageConsumptionAlgo) { if (!lots.length) return []; - const months = (monthAverageConsumption - 1 > -1 && monthAverageConsumption) - ? monthAverageConsumption - 1 : 0; - - const startDate = moment().subtract(months, 'month').toDate(); - const endDate = new Date(); + // NOTE(@jniles) - this is a developer sanity check. Fail _hard_ if the query is underspecified + // Throw an error if we don't have the monthAverageConsumption or averageConsumptionAlgo passed in. + if (!monthAverageConsumption || !averageConsumptionAlgo) { + throw new Error('Cannot calculate the AMC without consumption parameters!'); + } - // create a list of unique depot/inventory_uuid maps to avoid querying the server multiple - // times. + // create a list of unique depot/inventory_uuid combinations to avoid querying the server multiple + // times for the same inventory item. const params = _.chain(lots) - .map(row => [startDate, endDate, row.inventory_uuid, row.depot_uuid]) - .uniqBy(_.isEqual) + .map(row => ([monthAverageConsumption, row.inventory_uuid, row.depot_uuid])) + .uniqBy(row => row.toString()) .value(); - // collect the current cmm for the following inventory items. + // query the server const cmms = await Promise.all( - params.map(row => db.exec(`CALL getCMM(?,?, HUID(?), HUID(?))`, row).then(values => values[0][0])), + params.map(row => db.exec(`CALL getCMM(DATE_SUB(NOW(), INTERVAL ? MONTH), NOW(), HUID(?), HUID(?))`, row) + .then(values => values[0][0])), ); - const inventoryMap = _.groupBy(cmms, 'inventory_uuid'); + // create a map of the CMM values keys on the depot/inventory pairing. + const cmmMap = new Map(cmms.map(row => ([`${row.depot_uuid}-${row.inventory_uuid}`, row]))); + + // quick function to query the above map. + const getCMMForLot = (depotUuid, inventoryUuid) => cmmMap.get(`${depotUuid}-${inventoryUuid}`); lots.forEach(lot => { - const hasConsumption = inventoryMap[lot.inventory_uuid]; - if (hasConsumption) { - [lot.cmms] = hasConsumption; + const lotCMM = getCMMForLot(lot.depot_uuid, lot.inventory_uuid); + if (lotCMM) { + lot.cmms = lotCMM; lot.avg_consumption = lot.cmms[averageConsumptionAlgo]; } else { lot.cmms = {}; @@ -325,7 +335,9 @@ async function getBulkInventoryCMM(lots, monthAverageConsumption, averageConsump } }); - return lots; + // now that we have the CMMs correctly mapped, we can compute the inventory indicators + const result = computeInventoryIndicators(lots); + return result; } /** @@ -401,13 +413,13 @@ async function getMovements(depotUuid, params) { f.label AS flux_label, BUID(m.invoice_uuid) AS invoice_uuid, dm.text AS documentReference, BUID(m.stock_requisition_uuid) AS stock_requisition_uuid, sr_m.text AS document_requisition FROM stock_movement m - JOIN lot l ON l.uuid = m.lot_uuid - JOIN inventory i ON i.uuid = l.inventory_uuid - JOIN depot d ON d.uuid = m.depot_uuid - JOIN flux f ON f.id = m.flux_id - LEFT JOIN document_map dm ON dm.uuid = m.document_uuid - LEFT JOIN service AS serv ON serv.uuid = m.entity_uuid - LEFT JOIN document_map sr_m ON sr_m.uuid = m.stock_requisition_uuid + JOIN lot l ON l.uuid = m.lot_uuid + JOIN inventory i ON i.uuid = l.inventory_uuid + JOIN depot d ON d.uuid = m.depot_uuid + JOIN flux f ON f.id = m.flux_id + LEFT JOIN document_map dm ON dm.uuid = m.document_uuid + LEFT JOIN service AS serv ON serv.uuid = m.entity_uuid + LEFT JOIN document_map sr_m ON sr_m.uuid = m.stock_requisition_uuid `; const finalClause = 'GROUP BY document_uuid, is_exit'; @@ -462,63 +474,61 @@ function getLotsOrigins(depotUuid, params, averageConsumptionAlgo) { * @function computeInventoryIndicators * * @description - * Stock Management Processing + * This function acts on information coming from the getBulkInventoryCMM() function. It's + * separated for clarity. + * + * This could be either lots or inventory items passed in. + * + * Here is the order you should be executing these: + * getBulkInventoryCMM() + * computeInventoryIndicators() + * computeLotIndicators() * * DEFINITIONS: * S_SEC: Security Stock - one month of stock on hand based on the average consumption. * S_MIN: Minimum stock - typically the security stock (depends on the depot) - * S_RP: Risk of Expiration. */ function computeInventoryIndicators(inventories) { - let CM; - let Q; - let CM_NOT_ZERO; - for (let i = 0; i < inventories.length; i++) { const inventory = inventories[i]; // the quantity of stock available in the given depot - Q = inventory.quantity; // the quantity + const Q = inventory.quantity; // the quantity // Average Monthly Consumption (CMM/AMC) - // This is calculuated during the stock exit to a patient or a service - // It is _not_ the average of stock exits, as both movements to other depots - // and stock loss are not included in this. - CM = inventory.avg_consumption; // consommation mensuelle - CM_NOT_ZERO = !CM ? 1 : CM; + // This value is computed in the getBulkInventoryCMM() function. + // It provides the average monthly consumption for the particular product. + const CMM = inventory.avg_consumption; + + // Signal that no consumption has occurred of the inventory items + inventory.NO_CONSUMPTION = (CMM === 0); // Compute the Security Stock // Security stock is defined by taking the average monthly consumption (AMC or CMM) // and multiplying it by the Lead Time (inventory.delay). The Lead Time is by default 1 month. // This gives you a security stock quantity. - inventory.S_SEC = CM * inventory.delay; // stock de securite + inventory.S_SEC = CMM * inventory.delay; // stock de securite // Compute Minimum Stock // The minumum of stock required is double the security stock. + // NOTE(@jniles): this is defined per depot. inventory.S_MIN = inventory.S_SEC * inventory.min_months_security_stock; // Compute Maximum Stock // The maximum stock is the minumum stock plus the amount able to be consumed in a // single purchase interval. - inventory.S_MAX = (CM * inventory.purchase_interval) + inventory.S_MIN; // stock maximum + inventory.S_MAX = (CMM * inventory.purchase_interval) + inventory.S_MIN; // stock maximum // Compute Months of Stock Remaining // The months of stock remaining is the quantity in stock divided by the Average - // monthly consumption. - inventory.S_MONTH = Math.floor(inventory.quantity / CM_NOT_ZERO); // mois de stock + // monthly consumption. Skip division by zero if the CMM is 0. + inventory.S_MONTH = inventory.NO_CONSUMPTION ? null : Math.floor(inventory.quantity / CMM); // mois de stock // Compute the Refill Quantity // The refill quantity is the amount of stock needed to order to reach your maximum stock. inventory.S_Q = inventory.S_MAX - inventory.quantity; // Commande d'approvisionnement inventory.S_Q = inventory.S_Q > 0 ? parseInt(inventory.S_Q, 10) : 0; - // Compute the Risk of Stock Expiry - // The risk of stock expiry is the quantity of drugs that will expire before you - // can use them. - inventory.S_RP = inventory.quantity - (inventory.lifetime * CM); // risque peremption - - console.log('inventory:', inventory); - // compute the inventory status code if (Q <= 0) { inventory.status = 'stock_out'; @@ -554,11 +564,11 @@ function computeInventoryIndicators(inventories) { async function getDailyStockConsumption(params) { const consumptionValue = ` - (( + (i.consumable = 1 AND ( (m.flux_id IN (${flux.TO_PATIENT}, ${flux.TO_SERVICE})) OR - (m.flux_id=${flux.TO_OTHER_DEPOT} AND d.is_warehouse=1) - ) AND i.consumable=1) + (m.flux_id = ${flux.TO_OTHER_DEPOT} AND d.is_warehouse = 1) + )) `; db.convert(params, ['depot_uuid', 'inventory_uuid']); @@ -609,10 +619,8 @@ async function getDailyStockConsumption(params) { /** * Inventory Quantity and Consumptions */ -async function getInventoryQuantityAndConsumption(params, monthAverageConsumption, averageConsumptionAlgo) { +async function getInventoryQuantityAndConsumption(params) { let _status; - let delay; - let purchaseInterval; let requirePurchaseOrder; let emptyLotToken = ''; // query token to include/exclude empty lots @@ -621,16 +629,6 @@ async function getInventoryQuantityAndConsumption(params, monthAverageConsumptio delete params.status; } - if (params.inventory_delay) { - delay = params.inventory_delay; - delete params.inventory_delay; - } - - if (params.purchase_interval) { - purchaseInterval = params.purchase_interval; - delete params.purchase_interval; - } - if (params.require_po) { requirePurchaseOrder = params.require_po; delete params.require_po; @@ -653,7 +651,7 @@ async function getInventoryQuantityAndConsumption(params, monthAverageConsumptio BUID(l.inventory_uuid) AS inventory_uuid, BUID(l.origin_uuid) AS origin_uuid, l.entry_date, BUID(i.uuid) AS inventory_uuid, i.code, i.text, BUID(m.depot_uuid) AS depot_uuid, i.avg_consumption, i.purchase_interval, i.delay, MAX(m.created_at) AS last_movement_date, - iu.text AS unit_type, + iu.text AS unit_type, ig.tracking_consumption, ig.tracking_expiration, BUID(ig.uuid) AS group_uuid, ig.name AS group_name, dm.text AS documentReference, d.enterprise_id FROM stock_movement m @@ -668,14 +666,15 @@ async function getInventoryQuantityAndConsumption(params, monthAverageConsumptio const clause = ` GROUP BY l.inventory_uuid, m.depot_uuid ${emptyLotToken} ORDER BY ig.name, i.text `; let filteredRows = await getLots(sql, params, clause); + if (filteredRows.length === 0) { return []; } - const settingsql = `SELECT month_average_consumption FROM stock_setting WHERE enterprise_id = ?`; - const setting = await db.one(settingsql, filteredRows[0].enterprise_id); + const settingsql = ` + SELECT month_average_consumption, average_consumption_algo FROM stock_setting WHERE enterprise_id = ? + `; + const opts = await db.one(settingsql, filteredRows[0].enterprise_id); // add the CMM - await getBulkInventoryCMM(filteredRows, setting.month_average_consumption, averageConsumptionAlgo); - - filteredRows = computeInventoryIndicators(filteredRows, delay, purchaseInterval); + filteredRows = await getBulkInventoryCMM(filteredRows, opts.month_average_consumption, opts.average_consumption_algo); if (_status) { filteredRows = filteredRows.filter(row => row.status === _status); @@ -689,13 +688,12 @@ async function getInventoryQuantityAndConsumption(params, monthAverageConsumptio } /** + * @function computeLotIndicators * process multiple stock lots * * @description - * the goals of this function is to give the risk of expiration for each lots for - * a given inventory */ -function processMultipleLots(inventories) { +function computeLotIndicators(inventories) { const flattenLots = []; const inventoryByDepots = _.groupBy(inventories, 'depot_uuid'); @@ -745,7 +743,6 @@ function processMultipleLots(inventories) { flattenLots.push(lot); }); }); - }); return flattenLots; diff --git a/server/controllers/stock/index.js b/server/controllers/stock/index.js index 6e0fb6128b..e8e6557216 100644 --- a/server/controllers/stock/index.js +++ b/server/controllers/stock/index.js @@ -762,8 +762,8 @@ function dashboard(req, res, next) { async function listLotsDepot(req, res, next) { const params = req.query; - params.monthAverageConsumption = req.session.stock_settings.month_average_consumption; - params.averageConsumptionAlgo = req.session.stock_settings.average_consumption_algo; + params.month_average_consumption = req.session.stock_settings.month_average_consumption; + params.average_consumption_algo = req.session.stock_settings.average_consumption_algo; if (req.session.stock_settings.enable_strict_depot_permission) { params.check_user_id = req.session.user.id; @@ -811,19 +811,19 @@ async function listLotsDepot(req, res, next) { */ async function listInventoryDepot(req, res, next) { const params = req.query; - const monthAverageConsumption = req.session.stock_settings.month_average_consumption; - const averageConsumptionAlgo = req.session.stock_settings.average_consumption_algo; // expose connected user data if (req.session.stock_settings.enable_strict_depot_permission) { params.check_user_id = req.session.user.id; } - try { - const inventoriesParameters = [params, monthAverageConsumption, averageConsumptionAlgo]; + params.month_average_consumption = req.session.stock_settings.month_average_consumption; + params.average_consumption_algo = req.session.stock_settings.average_consumption_algo; + try { + // FIXME(@jniles) - these two call essentially the same route. Do we need both? const [inventories, lots] = await Promise.all([ - core.getInventoryQuantityAndConsumption(...inventoriesParameters), + core.getInventoryQuantityAndConsumption(params), core.getLotsDepot(null, params), ]); diff --git a/server/controllers/stock/reports/stock/lots_report.js b/server/controllers/stock/reports/stock/lots_report.js index 4e19b33980..c31bbbbfbe 100644 --- a/server/controllers/stock/reports/stock/lots_report.js +++ b/server/controllers/stock/reports/stock/lots_report.js @@ -45,12 +45,13 @@ function stockLotsReport(req, res, next) { delete options.defaultPeriod; } - options.monthAverageConsumption = req.session.stock_settings.month_average_consumption; - if (req.session.stock_settings.enable_strict_depot_permission) { options.check_user_id = req.session.user.id; } + options.month_average_consumption = req.session.stock_settings.month_average_consumption; + options.average_consumption_algo = req.session.stock_settings.average_consumption_algo; + return Stock.getLotsDepot(null, options) .then((rows) => { data.rows = rows; diff --git a/server/models/procedures/stock.sql b/server/models/procedures/stock.sql index 997e7422f6..18b1fa730e 100644 --- a/server/models/procedures/stock.sql +++ b/server/models/procedures/stock.sql @@ -668,6 +668,7 @@ CREATE PROCEDURE `getCMM` ( ROUND(IFNULL(@algo3, 0),2) as algo3, ROUND(IFNULL(@algo_msh, 0), 2) as algo_msh, BUID(_inventory_uuid) as inventory_uuid, + BUID(_depot_uuid) as depot_uuid, _start_date as start_date, _end_date as end_date, _first_inventory_mvt_date as first_inventory_movement_date, diff --git a/test/data.sql b/test/data.sql index 784a1c8890..c67127cce3 100644 --- a/test/data.sql +++ b/test/data.sql @@ -3002,7 +3002,7 @@ INSERT INTO `lot` (`uuid`, `label`, `initial_quantity`, `quantity`, `unit_cost`, (HUID('aca917fe-5320-4c3c-bea6-590e48cfa26b'), 'ERYTHRO-A', 10, 0, 3.1800, DATE_ADD(CURRENT_DATE, INTERVAL 3 MONTH), @erythromycine, HUID('1908da7a-7892-48d7-a924-be647e5215ef'), 0, DATE_ADD(CURRENT_DATE, INTERVAL -66 DAY)); -- stock settings (go with defaults) -INSERT INTO `stock_setting` (`enterprise_id`, `enable_auto_stock_accounting`) VALUES (1, 0); +INSERT INTO `stock_setting` (`enterprise_id`, `enable_auto_stock_accounting`, `month_average_consumption`, `average_consumption_algo`) VALUES (1, 0, 6, 'algo_msh'); -- stock lots movements INSERT INTO `stock_movement` (`uuid`, `lot_uuid`, `document_uuid`, `depot_uuid`, `entity_uuid`, `flux_id`, `date`, `quantity`, `unit_cost`, `is_exit`, `period_id`, `user_id`) VALUES diff --git a/test/end-to-end/stock/stock.inventories.js b/test/end-to-end/stock/stock.inventories.js index 073a30ac4d..eb993f5a21 100644 --- a/test/end-to-end/stock/stock.inventories.js +++ b/test/end-to-end/stock/stock.inventories.js @@ -65,23 +65,23 @@ function StockInventoriesRegistryTests() { await filters.resetFilters(); }); - it('find 2 inventories by state plus one line for grouping (minimum reached)', async () => { + it('find 0 inventories by state for grouping (minimum reached)', async () => { await FU.radio('$ctrl.searchQueries.status', 3); await FU.modal.submit(); - await GU.expectRowCount(gridId, 3); + await GU.expectRowCount(gridId, 0); await filters.resetFilters(); }); - it('find 3 inventories by state (over maximum)', async () => { + it('find 6 inventories by state (over maximum)', async () => { await FU.radio('$ctrl.searchQueries.status', 4); await FU.modal.submit(); - await GU.expectRowCount(gridId, 3); + await GU.expectRowCount(gridId, 6); await filters.resetFilters(); }); - it('find 9 inventories for all time ', async () => { + it('find 7 inventories for all time ', async () => { await modal.switchToDefaultFilterTab(); await modal.setPeriod('allTime'); await modal.submit(); @@ -89,10 +89,10 @@ function StockInventoriesRegistryTests() { await filters.resetFilters(); }); - it('find 3 inventories who requires a purchase order plus one line of grouping', async () => { + it('find 1 inventories who requires a purchase order plus one line of grouping', async () => { await element(by.model('$ctrl.searchQueries.require_po')).click(); await FU.modal.submit(); - await GU.expectRowCount(gridId, 3); + await GU.expectRowCount(gridId, 2); await filters.resetFilters(); }); } From f35575a1b1db08ad1bde8ce1e4d7af579bde0747 Mon Sep 17 00:00:00 2001 From: Jonathan Niles Date: Wed, 2 Dec 2020 06:26:56 +0100 Subject: [PATCH 4/4] fix(stock): adjust CMM tests december Fixes the fact that we have some months with 30 days and others with 31 days. --- test/integration-stock/depots.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration-stock/depots.js b/test/integration-stock/depots.js index 774fd87e91..0105be8eba 100644 --- a/test/integration-stock/depots.js +++ b/test/integration-stock/depots.js @@ -94,16 +94,16 @@ describe('(/depots) The depots API ', () => { res = await agent.get(`/depots/${principal}/inventories/${ampicilline}/cmm`); values = { - algo1 : 33.39, + algo1 : 33.27, algo2 : 1143.75, algo3 : 24.93, - algo_msh : 33.52, + algo_msh : 33.39, sum_days : 367, - sum_stock_day : 274, + sum_stock_day : 275, sum_consumption_day : 8, sum_consumed_quantity : 300, number_of_month : 12, - sum_stock_out_days : 93, + sum_stock_out_days : 92, days_before_consumption : 0, };