From 3234ce4092ce4d72de308ff1b76267fc68b28904 Mon Sep 17 00:00:00 2001 From: lomamech Date: Wed, 24 Mar 2021 14:07:43 +0100 Subject: [PATCH] feature(Prevent negative quantity) Prevent negative quantity in client side in - All stock exit - Inventory adjustement - Aggregate stock consumption --- client/src/i18n/en/errors.json | 1 + client/src/i18n/en/stock.json | 2 + client/src/i18n/fr/errors.json | 1 + client/src/i18n/fr/stock.json | 4 + .../aggregated_consumption.html | 11 +++ .../aggregated_consumption.js | 51 ++++++++++++ client/src/modules/stock/exit/exit.html | 11 +++ client/src/modules/stock/exit/exit.js | 74 +++++++++++++++-- .../inventory-adjustment.html | 11 +++ .../inventory-adjustment.js | 54 ++++++++++++ server/controllers/stock/index.js | 82 ++++++++++++++++++- 11 files changed, 292 insertions(+), 10 deletions(-) diff --git a/client/src/i18n/en/errors.json b/client/src/i18n/en/errors.json index b24d1079ed..e95f5f9f7d 100644 --- a/client/src/i18n/en/errors.json +++ b/client/src/i18n/en/errors.json @@ -25,6 +25,7 @@ "ER_BAD_FIELD_ERROR" : "Column does not exist in database.", "ER_ROW_IS_REFERENCED" : "Cannot delete entity because entity is used in another table.", "ER_ROW_IS_REFERENCED_2" : "Cannot delete entity because entity is used in another table.", + "ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK" : "There are stock movements, which may overconsume the quantity in stock and generate negative quantity in stock", "ER_BAD_NULL_ERROR" : "A column was left NULL that cannot be NULL.", "ER_PARSE_ERROR" : "Your request could not be translated into valid SQL. Please modify your request and try again.", "ER_EMPTY_QUERY" : "Your request seems empty. Are you sure you filled everything in?", diff --git a/client/src/i18n/en/stock.json b/client/src/i18n/en/stock.json index 26865977c2..a993903f66 100644 --- a/client/src/i18n/en/stock.json +++ b/client/src/i18n/en/stock.json @@ -69,6 +69,8 @@ "EXCESSIVE_QUANTITY": "Excessive Quantity", "LOT_QUANTITY_OVER_GLOBAL": "Total lot quantity is more than the global quantity", "MISSING_LOT_UNIT_COST": "A non-negative value is required for lot unit cost", + "OVERCONSUMED_INVENTORIES": "Over-consumed inventories", + "OVERCONSUMED_INVENTORIES_DESCRIPTION": "{{ text }} with batch number {{ label }}, actually only has {{ quantityAvailable }} {{ unit_type }}, consumption of {{ quantity }} {{ unit_type }} items is unrealistic", "PLEASE_CHECK_EXPIRY_DATE": "Please check the expiration date", "POORLY_FORMALIZED_DATE_RANGE": "Poorly formalized date range: one consumption period must not be contained in another", "QUANTITY_CONSUMED_LOWER" : "Total quantity consumed is lower than the global quantity consumed", diff --git a/client/src/i18n/fr/errors.json b/client/src/i18n/fr/errors.json index e89067bc46..c6c64a89a9 100644 --- a/client/src/i18n/fr/errors.json +++ b/client/src/i18n/fr/errors.json @@ -25,6 +25,7 @@ "ER_BAD_FIELD_ERROR" : "Vous avez entré un champ qui ne peut pas être stocké dans la base de données.", "ER_ROW_IS_REFERENCED" : "Vous ne pouvez pas supprimer cet enregistrement, car il est référencé par un autre enregistrement dans la base de données.", "ER_ROW_IS_REFERENCED_2" : "Vous ne pouvez pas supprimer cet enregistrement, car il est référencé par un autre enregistrement dans la base de données.", + "ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK" : "Il y a des mouvements de stock, qui peuvent surconsommer la quantité en stock et générer une quantité négative en stock", "ER_BAD_NULL_ERROR" : "Un champ a été laissé vide qui ne peut pas être vide.", "ER_PARSE_ERROR" : "Votre demande n'a pas été comprise. Veuillez modifier votre demande et réessayer.", "ER_EMPTY_QUERY" : "Votre demande semble vide. Êtes-vous sûr de remplir tout?", diff --git a/client/src/i18n/fr/stock.json b/client/src/i18n/fr/stock.json index f3a3975d51..b04956c376 100644 --- a/client/src/i18n/fr/stock.json +++ b/client/src/i18n/fr/stock.json @@ -70,7 +70,11 @@ "EXCESSIVE_QUANTITY": "Quantité excessive", "LOT_QUANTITY_OVER_GLOBAL": "La quantité totale des lots dépasse la quantity globale fournie", "MISSING_LOT_UNIT_COST": "Une valeur non négative est requise pour le coût unitaire du lot", + "OVERCONSUMED_INVENTORIES": "Inventaires surconsommés", + "OVERCONSUMED_INVENTORIES_DESCRIPTION": "{{ text }} avec le numéro de lot {{ label }}, ne possède en réalité que {{ quantityAvailable }} {{ unit_type }} la consommation de {{ quantity }} {{ unit_type }} n'est pas réaliste", "PLEASE_CHECK_EXPIRY_DATE": "Veuillez vérifier la date d'expiration", + "PROBLEMATIC_ADJUSTMENT": "Ajustement problematique", + "PROBLEMATIC_ADJUSTMENT_DESCRIPTION": "{{ text }} avec le numéro de lot {{ label }}, ne possède en réalité que {{ quantityAvailable }} l'ajustement négatif de {{ old_quantity }} à {{ quantity }} générera une quantité en stock négative", "POORLY_FORMALIZED_DATE_RANGE": "Plages des dates males formalisées: une période de consommation ne doit pas être contenue dans une autre", "QUANTITY_CONSUMED_LOWER" : "La quantité totale consommé est inférieure à la quantity globale consommée", "QUANTITY_CONSUMED_OVER_GLOBAL" : "La quantité totale consommé dépasse la quantity globale consommée", diff --git a/client/src/modules/stock/aggregated_consumption/aggregated_consumption.html b/client/src/modules/stock/aggregated_consumption/aggregated_consumption.html index 4d6c396636..e71a456b25 100644 --- a/client/src/modules/stock/aggregated_consumption/aggregated_consumption.html +++ b/client/src/modules/stock/aggregated_consumption/aggregated_consumption.html @@ -59,6 +59,17 @@
+ +
+
+ STOCK.ERRORS.OVERCONSUMED_INVENTORIES +
    +
  • + STOCK.ERRORS.OVERCONSUMED_INVENTORIES_DESCRIPTION +
  • +
+
+

STOCK.AGGREGATED_STOCK_CONSUMPTION.TITLE

diff --git a/client/src/modules/stock/aggregated_consumption/aggregated_consumption.js b/client/src/modules/stock/aggregated_consumption/aggregated_consumption.js index f69b85ab17..9d46219200 100644 --- a/client/src/modules/stock/aggregated_consumption/aggregated_consumption.js +++ b/client/src/modules/stock/aggregated_consumption/aggregated_consumption.js @@ -29,6 +29,9 @@ function StockAggregatedConsumptionController( vm.setConsumptionByLots = setConsumptionByLots; vm.checkValidation = checkValidation; + vm.currentInventories = []; + vm.overconsumption = []; + vm.onSelectFiscalYear = (fiscalYear) => { setupStock(); vm.movement.fiscal_id = fiscalYear.id; @@ -48,11 +51,13 @@ function StockAggregatedConsumptionController( vm.movement.period_id = period.id; loadInventories(vm.depot); + loadCurrentInventories(vm.depot); }; vm.onChangeDepot = depot => { vm.depot = depot; loadInventories(vm.depot); + loadCurrentInventories(vm.depot); }; /** @@ -228,6 +233,21 @@ function StockAggregatedConsumptionController( }); } + function loadCurrentInventories(depot, dateTo = new Date()) { + vm.loading = true; + Stock.lots.read(null, { depot_uuid : depot.uuid, dateTo }) + .then(lots => { + vm.currentInventories = lots.filter(item => item.quantity > 0); + + // Here we check directly if a Depot has inventories in stock available in current date + vm.emptyCurrentStock = !vm.currentInventories.length; + }) + .catch(Notify.handleError) + .finally(() => { + vm.loading = false; + }); + } + function checkValidation(consumptionData) { let valid = true; @@ -249,6 +269,37 @@ function StockAggregatedConsumptionController( user : Session.user.display_name, }; + const checkOverconsumption = vm.Stock.store.data; + + checkOverconsumption.forEach(stock => { + stock.quantityAvailable = 0; + + vm.currentInventories.forEach(lot => { + if (lot.uuid === stock.uuid) { + stock.quantityAvailable = lot.quantity; + } + }); + }); + + vm.overconsumption = checkOverconsumption.filter( + c => (c.quantity_consumed + c.quantity_lost) > c.quantityAvailable, + ); + + if (vm.overconsumption.length) { + vm.overconsumption.forEach(item => { + item.textI18n = { + text : item.text, + label : item.label, + quantityAvailable : item.quantityAvailable, + quantity : (item.quantity_consumed + item.quantity_lost), + }; + }); + + Notify.danger('ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK'); + vm.$loading = false; + return 0; + } + const formatedDescription = $translate.instant('STOCK.EXIT_AGGREGATE_CONSUMPTION', i18nKeys); const isValid = vm.Stock.validate(); diff --git a/client/src/modules/stock/exit/exit.html b/client/src/modules/stock/exit/exit.html index 4e6ee5f4b0..e9ff4147fe 100644 --- a/client/src/modules/stock/exit/exit.html +++ b/client/src/modules/stock/exit/exit.html @@ -87,6 +87,17 @@
+ +
+
+ STOCK.ERRORS.OVERCONSUMED_INVENTORIES +
    +
  • + STOCK.ERRORS.OVERCONSUMED_INVENTORIES_DESCRIPTION +
  • +
+
+
diff --git a/client/src/modules/stock/exit/exit.js b/client/src/modules/stock/exit/exit.js index a02d0a246c..f30cebe28e 100644 --- a/client/src/modules/stock/exit/exit.js +++ b/client/src/modules/stock/exit/exit.js @@ -29,8 +29,10 @@ function StockExitController( vm.gridApi = {}; vm.selectedLots = []; vm.selectableInventories = []; + vm.currentInventories = []; vm.reset = reset; vm.ROW_ERROR_FLAG = bhConstants.grid.ROW_ERROR_FLAG; + vm.overconsumption = []; vm.onDateChange = date => { vm.movement.date = date; @@ -38,12 +40,18 @@ function StockExitController( vm.dateMessageWarning = true; } loadInventories(vm.depot, date); + loadCurrentInventories(vm.depot); + + vm.overconsumption = []; checkValidity(); }; vm.onChangeDepot = depot => { vm.depot = depot; loadInventories(vm.depot); + loadCurrentInventories(vm.depot); + + vm.overconsumption = []; }; // bind methods @@ -206,6 +214,8 @@ function StockExitController( vm.movement.description = $translate.instant(mapExit[exitType.label].description); vm.stockForm.store.clear(); vm.resetEntryExitTypes = false; + + vm.overconsumption = []; } function setupStock() { @@ -284,6 +294,22 @@ function StockExitController( }); } + function loadCurrentInventories(depot, dateTo = new Date()) { + vm.loading = true; + Stock.lots.read(null, { depot_uuid : depot.uuid, dateTo }) + .then(lots => { + + vm.currentInventories = lots.filter(item => item.quantity > 0); + + // Here we check directly if a Depot has inventories in stock available in current date + vm.emptyCurrentStock = !vm.currentInventories.length; + }) + .catch(Notify.handleError) + .finally(() => { + vm.loading = false; + }); + } + // on lot select function onLotSelect(row) { if (!row.lot || !row.lot.uuid) { return; } @@ -399,12 +425,14 @@ function StockExitController( } function setSelectedEntity(entity) { - const uniformEntity = Stock.uniformSelectedEntity(entity); - vm.reference = uniformEntity.reference; - vm.displayName = uniformEntity.displayName; - vm.selectedEntityUuid = uniformEntity.uuid; - vm.requisition = (entity && entity.requisition) || {}; - loadRequisitions(entity); + if (entity) { + const uniformEntity = Stock.uniformSelectedEntity(entity); + vm.reference = uniformEntity.reference; + vm.displayName = uniformEntity.displayName; + vm.selectedEntityUuid = uniformEntity.uuid; + vm.requisition = (entity && entity.requisition) || {}; + loadRequisitions(entity); + } } function loadRequisitions(entity) { @@ -450,6 +478,36 @@ function StockExitController( function submit(form) { if (form.$invalid) { return null; } + const checkOverconsumption = vm.stockForm.store.data; + + checkOverconsumption.forEach(stock => { + stock.quantityAvailable = 0; + + vm.currentInventories.forEach(lot => { + if (lot.uuid === stock.lot.uuid) { + stock.quantityAvailable = lot.quantity; + } + }); + }); + + vm.overconsumption = checkOverconsumption.filter(c => c.quantity > c.quantityAvailable); + + if (vm.overconsumption.length) { + vm.overconsumption.forEach(item => { + item.textI18n = { + text : item.inventory.text, + label : item.lot.label, + quantityAvailable : item.quantityAvailable, + quantity : item.quantity, + unit_type : item.inventory.unit_type, + }; + }); + + Notify.danger('ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK'); + vm.$loading = false; + return 0; + } + if (vm.movement.exit_type !== 'loss' && expiredLots()) { // NOTE: This check may not be necessary, since the user cannot select // expired lots/batches directly. But lots can also come in via @@ -477,8 +535,12 @@ function StockExitController( // Load inventories loadInventories(vm.depot); + // Load current inventories for antedate cases + loadCurrentInventories(vm.depot); + vm.reset(form); vm.selectedLots = []; + vm.overconsumption = []; resetSelectedEntity(); } diff --git a/client/src/modules/stock/inventory-adjustment/inventory-adjustment.html b/client/src/modules/stock/inventory-adjustment/inventory-adjustment.html index 300b4920c4..37203fbe8f 100644 --- a/client/src/modules/stock/inventory-adjustment/inventory-adjustment.html +++ b/client/src/modules/stock/inventory-adjustment/inventory-adjustment.html @@ -55,6 +55,17 @@
+ +
+
+ STOCK.ERRORS.PROBLEMATIC_ADJUSTMENT +
    +
  • + STOCK.ERRORS.PROBLEMATIC_ADJUSTMENT_DESCRIPTION +
  • +
+
+

INVENTORY_ADJUSTMENT.TITLE

diff --git a/client/src/modules/stock/inventory-adjustment/inventory-adjustment.js b/client/src/modules/stock/inventory-adjustment/inventory-adjustment.js index f04d0b850e..d52307527c 100644 --- a/client/src/modules/stock/inventory-adjustment/inventory-adjustment.js +++ b/client/src/modules/stock/inventory-adjustment/inventory-adjustment.js @@ -27,14 +27,23 @@ function StockInventoryAdjustmentController( vm.movement = {}; vm.stockOut = {}; + vm.currentInventories = []; + vm.overconsumption = []; + vm.onDateChange = date => { vm.movement.date = date; loadInventories(vm.depot); + loadCurrentInventories(vm.depot); + + vm.overconsumption = []; }; vm.onChangeDepot = depot => { vm.depot = depot; loadInventories(vm.depot); + loadCurrentInventories(vm.depot); + + vm.overconsumption = []; }; // bind constants @@ -197,6 +206,21 @@ function StockInventoryAdjustmentController( }); } + function loadCurrentInventories(depot, dateTo = new Date()) { + vm.loading = true; + Stock.lots.read(null, { depot_uuid : depot.uuid, dateTo }) + .then(lots => { + vm.currentInventories = lots.filter(item => item.quantity > 0); + + // Here we check directly if a Depot has inventories in stock available in current date + vm.emptyCurrentStock = !vm.currentInventories.length; + }) + .catch(Notify.handleError) + .finally(() => { + vm.loading = false; + }); + } + // ================================= Submit ================================ function submit(form) { // check stock validity @@ -218,6 +242,36 @@ function StockInventoryAdjustmentController( return row; }); + const checkOverconsumption = vm.Stock.store.data; + + checkOverconsumption.forEach(stock => { + stock.quantityAvailable = 0; + + vm.currentInventories.forEach(lot => { + if (lot.uuid === stock.uuid) { + stock.quantityAvailable = lot.quantity; + } + }); + }); + + vm.overconsumption = checkOverconsumption.filter(c => (c.old_quantity - c.quantity) > c.quantityAvailable); + + if (vm.overconsumption.length) { + vm.overconsumption.forEach(item => { + item.textI18n = { + text : item.text, + label : item.label, + old_quantity : item.old_quantity, + quantityAvailable : item.quantityAvailable, + quantity : item.quantity, + }; + }); + + Notify.danger('ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK'); + vm.$loading = false; + return 0; + } + movement.lots = lots.filter(lot => { return lot.quantity !== lot.oldQuantity; }); diff --git a/server/controllers/stock/index.js b/server/controllers/stock/index.js index 3089a21141..d131edc7fb 100644 --- a/server/controllers/stock/index.js +++ b/server/controllers/stock/index.js @@ -271,6 +271,17 @@ function createIntegration(req, res, next) { async function createInventoryAdjustment(req, res, next) { try { const movement = req.body; + let filteredInvalidData = []; + + const paramsStock = { + dateTo : new Date(), + depot_uuid : movement.depot_uuid, + includeEmptyLot : 0, + month_average_consumption : req.session.stock_settings.month_average_consumption, + average_consumption_algo : req.session.stock_settings.average_consumption_algo, + }; + + const stockAvailable = await core.getLotsDepot(null, paramsStock); if (!movement.depot_uuid) { throw new Error('No defined depot'); @@ -283,14 +294,39 @@ async function createInventoryAdjustment(req, res, next) { const period = await Fiscal.lookupFiscalYearByDate(new Date(movement.date)); const periodId = period.id; - // pass reverse operations const trx = db.transaction(); - const uniqueAdjustmentUuid = uuid(); let countNeedIncrease = 0; let countNeedDecrease = 0; + lots.forEach(lot => { + lot.quantityAvailable = 0; + + if (lot.oldQuantity > lot.quantity) { + lot.isExit = 1; + lot.outputQuantity = lot.oldQuantity - lot.quantity; + + if (stockAvailable) { + stockAvailable.forEach(stock => { + if (stock.uuid === lot.uuid) { + lot.quantityAvailable = stock.quantity; + } + }); + } + } + }); + + filteredInvalidData = await lots.filter(l => l.outputQuantity > l.quantityAvailable); + + if (filteredInvalidData.length) { + throw new BadRequest( + `There are stock movements, + which may overconsume the quantity in stock and generate negative quantity in stock`, + `ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK`, + ); + } + lots.forEach(lot => { const difference = lot.quantity - lot.oldQuantity; @@ -413,8 +449,11 @@ async function createMovement(req, res, next) { try { if (filteredInvalidData.length) { - throw new BadRequest(`Invalid data! There are stock movements, - which may overconsume the quantity in stock and generate negative quantity in stock.`); + throw new BadRequest( + `There are stock movements, + which may overconsume the quantity in stock and generate negative quantity in stock`, + `ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK`, + ); } const periodId = (await Fiscal.lookupFiscalYearByDate(params.date)).id; @@ -1051,6 +1090,41 @@ function getStockTransfers(req, res, next) { async function createAggregatedConsumption(req, res, next) { try { const movement = req.body; + let filteredInvalidData = []; + + const paramsStock = { + dateTo : new Date(), + depot_uuid : movement.depot_uuid, + includeEmptyLot : 0, + month_average_consumption : req.session.stock_settings.month_average_consumption, + average_consumption_algo : req.session.stock_settings.average_consumption_algo, + }; + + const stockAvailable = await core.getLotsDepot(null, paramsStock); + + movement.lots.forEach(lot => { + lot.quantityAvailable = 0; + + lot.outputQuantity = lot.quantity_consumed + lot.quantity_lost; + + if (stockAvailable) { + stockAvailable.forEach(stock => { + if (stock.uuid === lot.uuid) { + lot.quantityAvailable = stock.quantity; + } + }); + } + }); + + filteredInvalidData = await movement.lots.filter(l => l.outputQuantity > l.quantityAvailable); + + if (filteredInvalidData.length) { + throw new BadRequest( + `There are stock movements, + which may overconsume the quantity in stock and generate negative quantity in stock`, + `ERRORS.ER_PREVENT_NEGATIVE_QUANTITY_IN_STOCK`, + ); + } if (!movement.depot_uuid) { throw new Error('No defined depot');