/* * * * Copyright © 2013 VillageReach. All Rights Reserved. This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * * * * If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. * */ package org.openlmis.rnr.domain; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.apache.commons.collections.Predicate; import org.apache.commons.lang3.StringUtils; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.openlmis.core.domain.*; import org.openlmis.core.exception.DataException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.math.BigDecimal; import java.math.MathContext; import java.util.ArrayList; import java.util.List; import static java.lang.Math.floor; import static java.lang.Math.round; import static java.math.BigDecimal.valueOf; import static java.math.RoundingMode.HALF_UP; import static org.apache.commons.collections.CollectionUtils.find; import static com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY; import static org.openlmis.rnr.domain.ProgramRnrTemplate.*; import static org.openlmis.rnr.domain.Rnr.RNR_VALIDATION_ERROR; import static org.openlmis.rnr.domain.RnrStatus.AUTHORIZED; /** * This class represents the data captured against a product for each Requisition and contains methods to determine * normalisedConsumption, averageMonthlyConsumption, stockOutDays, packsToShip, orderQuantity, maxStockQuantity and * quantityDispensed of that product. */ @Data @NoArgsConstructor @JsonSerialize(include = NON_EMPTY) @EqualsAndHashCode(callSuper = true) public class RnrLineItem extends LineItem { public static final BigDecimal NUMBER_OF_DAYS = new BigDecimal(30); public static final MathContext MATH_CONTEXT = new MathContext(12, HALF_UP); public static final String DISPENSED_PLUS_NEW_PATIENTS = "DISPENSED_PLUS_NEW_PATIENTS"; public static final String DISPENSED_X_90 = "DISPENSED_X_90"; public static final String DEFAULT = "DEFAULT"; public static final String DISPENSED_X_2 = "DISPENSED_X_2"; public static final String CONSUMPTION_X_2 = "CONSUMPTION_X_2"; private static final Logger LOGGER = LoggerFactory.getLogger(RnrLineItem.class); //TODO : hack to display it on UI. This is concatenated string of Product properties like name, strength, form and dosage unit private String product; private Integer productDisplayOrder; private String productCode; private String productPrimaryName; private String productCategory; private Integer productCategoryDisplayOrder; private String productStrength; private Boolean roundToZero; private Integer packRoundingThreshold; private Integer packSize; private Integer dosesPerMonth; private Integer dosesPerDispensingUnit; private String dispensingUnit; private Double maxMonthsOfStock; private Boolean fullSupply; private Integer quantityReceived; private Integer quantityDispensed; private Integer previousStockInHand; private Integer beginningBalance; private List<LossesAndAdjustments> lossesAndAdjustments = new ArrayList<>(); private Integer totalLossesAndAdjustments = 0; private Integer stockInHand; private Integer stockOutDays; private Integer newPatientCount; private Integer quantityRequested; private String reasonForRequestedQuantity; private Integer amc; private Integer normalizedConsumption; private Integer periodNormalizedConsumption; private Integer calculatedOrderQuantity; private Integer maxStockQuantity; private Integer quantityApproved; private Integer reportingDays; private Integer packsToShip; private String expirationDate; private String remarks; private List<Integer> previousNormalizedConsumptions = new ArrayList<>(); private Money price; private Integer total; @SuppressWarnings("unused") private Boolean skipped = false; public RnrLineItem(Long rnrId, FacilityTypeApprovedProduct facilityTypeApprovedProduct, Long modifiedBy, Long createdBy) { this.rnrId = rnrId; this.maxMonthsOfStock = facilityTypeApprovedProduct.getMaxMonthsOfStock(); this.populateFromProduct(facilityTypeApprovedProduct.getProgramProduct()); this.modifiedBy = modifiedBy; this.createdBy = createdBy; } public void setFieldsForApproval() { if (this.skipped) { this.quantityReceived = null; this.quantityDispensed = null; this.beginningBalance = null; this.lossesAndAdjustments = new ArrayList<>(); this.totalLossesAndAdjustments = 0; this.stockInHand = null; this.stockOutDays = null; this.newPatientCount = null; this.quantityRequested = null; this.quantityApproved = null; this.reasonForRequestedQuantity = null; this.normalizedConsumption = null; this.periodNormalizedConsumption = null; this.packsToShip = null; this.remarks = null; this.expirationDate = null; } if(quantityApproved == null){ quantityApproved = (quantityRequested == null) ? calculatedOrderQuantity : quantityRequested; } } public void setBeginningBalanceWhenPreviousStockInHandAvailable(RnrLineItem previousLineItem, Boolean beginningBalanceVisible) { if (previousLineItem == null || (!beginningBalanceVisible && previousLineItem.getSkipped())) { this.beginningBalance = 0; return; } this.beginningBalance = previousLineItem.getStockInHand(); this.previousStockInHand = previousLineItem.getStockInHand(); } public void setSkippedValueWhenPreviousLineItemIsAvailable(RnrLineItem previousLineItem){ if(previousLineItem != null){ this.setSkipped(previousLineItem.getSkipped()); } } public void setLineItemFieldsAccordingToTemplate(ProgramRnrTemplate template) { if (!template.columnsVisible(QUANTITY_RECEIVED)) quantityReceived = 0; if (!template.columnsVisible(QUANTITY_DISPENSED)) quantityDispensed = 0; totalLossesAndAdjustments = 0; newPatientCount = 0; stockOutDays = 0; if(template.getApplyDefaultZero()){ quantityReceived = quantityDispensed = stockInHand = calculatedOrderQuantity = 0; if(beginningBalance == null){ beginningBalance = 0; } else{ stockInHand = beginningBalance; } } totalLossesAndAdjustments = newPatientCount = stockOutDays = 0; } public void validateForApproval() { if (!skipped && quantityApproved == null) throw new DataException(RNR_VALIDATION_ERROR); } public void validateMandatoryFields(ProgramRnrTemplate template) { String[] nonNullableFields = {BEGINNING_BALANCE, QUANTITY_RECEIVED, STOCK_IN_HAND, QUANTITY_DISPENSED, NEW_PATIENT_COUNT, STOCK_OUT_DAYS}; for (String fieldName : nonNullableFields) { if (template.columnsVisible(fieldName) && !template.columnsCalculated(fieldName) && (getValueFor(fieldName) == null || (Integer) getValueFor(fieldName) < 0)) { throw new DataException(RNR_VALIDATION_ERROR); } } requestedQuantityConditionalValidation(template); } public void validateNonFullSupply() { if (!(quantityRequested != null && quantityRequested >= 0 && reasonForRequestedQuantity != null)) { throw new DataException(RNR_VALIDATION_ERROR); } } public void validateCalculatedFields(ProgramRnrTemplate template) { boolean validQuantityDispensed = true; RnrColumn rnrColumn = (RnrColumn) template.getColumns().get(0); if (rnrColumn.isFormulaValidationRequired()) { validQuantityDispensed = (quantityDispensed == (beginningBalance + quantityReceived + totalLossesAndAdjustments - stockInHand)); } boolean valid = quantityDispensed >= 0 && stockInHand >= 0 && validQuantityDispensed; if (!valid) throw new DataException(RNR_VALIDATION_ERROR); } public void calculateForFullSupply(ProgramRnrTemplate template, RnrStatus rnrStatus, List<LossesAndAdjustmentsType> lossesAndAdjustmentsTypes, Integer numberOfMonths) { calculateTotalLossesAndAdjustments(lossesAndAdjustmentsTypes); if (template.columnsCalculated(STOCK_IN_HAND)) { calculateStockInHand(); } if (template.columnsCalculated(QUANTITY_DISPENSED)) { calculateQuantityDispensed(); } calculateNormalizedConsumption(template); calculatePeriodNormalizedConsumption(numberOfMonths); if (rnrStatus == AUTHORIZED) { calculateAmc(numberOfMonths); calculateMaxStockQuantity(template); if (!(template.getRnrColumnsMap().get(CALCULATED_ORDER_QUANTITY) != null && template.columnsUserInput(CALCULATED_ORDER_QUANTITY))) { calculateOrderQuantity(); } } calculatePacksToShip(); } public void calculatePeriodNormalizedConsumption(Integer numberOfMonths) { periodNormalizedConsumption = normalizedConsumption * numberOfMonths; } public void calculateAmc(Integer numberOfMonths) { Integer sumOfNCs = normalizedConsumption; for (Integer previousNC : previousNormalizedConsumptions) { sumOfNCs += previousNC; } BigDecimal countOfNCs = new BigDecimal((previousNormalizedConsumptions.size() + 1) * numberOfMonths); amc = new BigDecimal(sumOfNCs).divide(countOfNCs, MATH_CONTEXT).setScale(0, HALF_UP).intValue(); } public void calculatePacksToShip() { Integer orderQuantity = getOrderQuantity(); if (allNotNull(orderQuantity, packSize)) { packsToShip = ((orderQuantity == 0) ? (roundToZero ? 0 : 1) : applyRoundingRules(orderQuantity)); } } public void calculateMaxStockQuantity( ProgramRnrTemplate template) { RnrColumn column = template.getRnrColumnsMap().get("maxStockQuantity"); String columnOption = DEFAULT; if(column != null){ columnOption = column.getCalculationOption(); } if(CONSUMPTION_X_2.equalsIgnoreCase(columnOption)){ maxStockQuantity = this.normalizedConsumption * 2; }else if(DISPENSED_X_2.equalsIgnoreCase(columnOption)){ maxStockQuantity = this.quantityDispensed * 2; } else{ // apply the default calculation if there was no other calculation that works here maxStockQuantity = (int) round(maxMonthsOfStock * amc); } } public void calculateOrderQuantity() { if (allNotNull(maxStockQuantity, stockInHand)) { calculatedOrderQuantity = ((maxStockQuantity - stockInHand) < 0) ? 0 : maxStockQuantity - stockInHand; } } public void calculateNormalizedConsumption(ProgramRnrTemplate template) { prepareFieldsForCalculation(); RnrColumn column = template.getRnrColumnsMap().get("normalizedConsumption"); String selectedColumnOption = DEFAULT; if (column != null) { selectedColumnOption = column.getCalculationOption(); } if (DISPENSED_PLUS_NEW_PATIENTS.equalsIgnoreCase(selectedColumnOption)) { normalizedConsumption = quantityDispensed + newPatientCount; } else if (DISPENSED_X_90.equalsIgnoreCase(selectedColumnOption)) { if (stockOutDays < 90) { normalizedConsumption = (new BigDecimal(90 * quantityDispensed) .divide( new BigDecimal(90 - stockOutDays) , MATH_CONTEXT) ).intValue(); } else { normalizedConsumption = (90 * quantityDispensed); } } else { BigDecimal dosesPerDispensingUnit = new BigDecimal(Math.max(1, this.dosesPerDispensingUnit)); normalizedConsumption = calculateNormalizedConsumption( new BigDecimal(stockOutDays), new BigDecimal(quantityDispensed), new BigDecimal(newPatientCount), new BigDecimal(dosesPerMonth), dosesPerDispensingUnit, reportingDays, template); } } private void prepareFieldsForCalculation() { // prepare fields for calculation if(stockOutDays == null){ stockOutDays = 0; } if(newPatientCount == null){ newPatientCount = 0; } } public void calculateTotalLossesAndAdjustments(List<LossesAndAdjustmentsType> lossesAndAdjustmentsTypes) { if (lossesAndAdjustments.isEmpty()) { return; } Integer total = 0; for (LossesAndAdjustments lossAndAdjustment : lossesAndAdjustments) { if (getAdditive(lossAndAdjustment, lossesAndAdjustmentsTypes)) { total += lossAndAdjustment.getQuantity(); } else { total -= lossAndAdjustment.getQuantity(); } } totalLossesAndAdjustments = total; } public void calculateQuantityDispensed() { if (allNotNull(beginningBalance, quantityReceived, totalLossesAndAdjustments, stockInHand)) { quantityDispensed = beginningBalance + quantityReceived + totalLossesAndAdjustments - stockInHand; } } public void calculateStockInHand() { stockInHand = beginningBalance + quantityReceived + totalLossesAndAdjustments - quantityDispensed; } public Money calculateCost() { if (packsToShip != null && price != null) { return price.multiply(valueOf(packsToShip)); } return new Money("0"); } public void copyCreatorEditableFieldsForFullSupply(RnrLineItem lineItem, ProgramRnrTemplate template) { this.previousStockInHand = lineItem.previousStockInHand; copyTotalLossesAndAdjustments(lineItem, template); for (Column column : template.getColumns()) { String fieldName = column.getName(); if (fieldName.equals(QUANTITY_APPROVED)) continue; copyField(fieldName, lineItem, template); } } public void copyCreatorEditableFieldsForNonFullSupply(RnrLineItem lineItem, ProgramRnrTemplate template) { String[] editableFields = {QUANTITY_REQUESTED, REMARKS, REASON_FOR_REQUESTED_QUANTITY}; for (String fieldName : editableFields) { copyField(fieldName, lineItem, template); } } public void copyApproverEditableFields(RnrLineItem lineItem, ProgramRnrTemplate template) { String[] approverEditableFields = {QUANTITY_APPROVED, REMARKS, SKIPPED}; for (String fieldName : approverEditableFields) { copyField(fieldName, lineItem, template); } } public void addLossesAndAdjustments(LossesAndAdjustments lossesAndAdjustments) { this.lossesAndAdjustments.add(lossesAndAdjustments); } private Integer calculateNormalizedConsumption(BigDecimal stockOutDays, BigDecimal quantityDispensed, BigDecimal newPatientCount, BigDecimal dosesPerMonth, BigDecimal dosesPerDispensingUnit, Integer reportingDays, ProgramRnrTemplate template) { BigDecimal newPatientFactor; if (template.getRnrColumnsMap().get("newPatientCount").getConfiguredOption() != null && template.getRnrColumnsMap().get("newPatientCount").getConfiguredOption().getName().equals("newPatientCount")) { newPatientFactor = newPatientCount.multiply(dosesPerMonth.divide(dosesPerDispensingUnit, MATH_CONTEXT) .setScale(0, HALF_UP)); } else { newPatientFactor = newPatientCount; } if (reportingDays == null || stockOutDays.compareTo(new BigDecimal(reportingDays)) >= 0) { return quantityDispensed.add(newPatientFactor).setScale(0, HALF_UP).intValue(); } BigDecimal stockOutFactor = quantityDispensed.multiply(NUMBER_OF_DAYS .divide((new BigDecimal(reportingDays).subtract(stockOutDays)), MATH_CONTEXT)); return stockOutFactor.add(newPatientFactor).setScale(0, HALF_UP).intValue(); } private void copyField(String fieldName, RnrLineItem lineItem, ProgramRnrTemplate template) { if (!template.columnsVisible(fieldName) || !template.columnsUserInput(fieldName)) { return; } try { Field field = this.getClass().getDeclaredField(fieldName); field.set(this, field.get(lineItem)); } catch (Exception e) { LOGGER.error("Error in copying RnrLineItem's field", e); } } private void copyTotalLossesAndAdjustments(RnrLineItem item, ProgramRnrTemplate template) { if (template.columnsVisible(LOSSES_AND_ADJUSTMENTS)) this.totalLossesAndAdjustments = item.totalLossesAndAdjustments; } public void populateFromProduct(ProgramProduct programProduct) { Product product = programProduct.getProduct(); ProductCategory category = programProduct.getProductCategory(); this.price = programProduct.getCurrentPrice(); this.dosesPerMonth = programProduct.getDosesPerMonth(); this.productCode = product.getCode(); this.dispensingUnit = product.getDispensingUnit(); this.dosesPerDispensingUnit = product.getDosesPerDispensingUnit(); this.packSize = product.getPackSize(); this.roundToZero = product.getRoundToZero(); this.packRoundingThreshold = product.getPackRoundingThreshold(); this.product = product.getName(); this.fullSupply = programProduct.isFullSupply(); this.productDisplayOrder = programProduct.getDisplayOrder(); this.productCategory = category.getName(); this.productCategoryDisplayOrder = category.getDisplayOrder(); } private void requestedQuantityConditionalValidation(ProgramRnrTemplate template) { if (template.columnsVisible(QUANTITY_REQUESTED) && quantityRequested != null && StringUtils.isEmpty(reasonForRequestedQuantity)) { throw new DataException(RNR_VALIDATION_ERROR); } } private Object getValueFor(String fieldName) { Object value = null; try { Field field = this.getClass().getDeclaredField(fieldName); value = field.get(this); } catch (Exception e) { LOGGER.error("Error in reading RnrLineItem's field", e); } return value; } @Override public boolean compareCategory(LineItem lineItem) { return this.getProductCategory().equals(((RnrLineItem) lineItem).getProductCategory()); } @Override public String getCategoryName() { return this.productCategory; } @Override public String getValue(String columnName) throws NoSuchFieldException, IllegalAccessException { if (columnName.equals("lossesAndAdjustments")) { return this.getTotalLossesAndAdjustments().toString(); } if (columnName.equals("cost")) { return this.calculateCost().toString(); } if (columnName.equals("price")) { return this.getPrice().toString(); } if (columnName.equals("total") && this.getBeginningBalance() != null && this.getQuantityReceived() != null) { return String.valueOf((this.getBeginningBalance() + this.getQuantityReceived())); } Field field = RnrLineItem.class.getDeclaredField(columnName); field.setAccessible(true); Object fieldValue = field.get(this); return (fieldValue == null) ? "" : fieldValue.toString(); } @Override public boolean isRnrLineItem() { return true; } private Integer getOrderQuantity() { if (quantityApproved != null) return quantityApproved; if (quantityRequested != null) return quantityRequested; else return calculatedOrderQuantity; } private Integer applyRoundingRules(Integer orderQuantity) { Double packsToShip = floor(orderQuantity / packSize); Integer remainderQuantity = orderQuantity % packSize; if (remainderQuantity >= packRoundingThreshold) { packsToShip += 1; } if (packsToShip == 0 && !roundToZero) { packsToShip = 1d; } return packsToShip.intValue(); } private boolean allNotNull(Integer... fields) { for (Integer field : fields) { if (field == null) return false; } return true; } private Boolean getAdditive(final LossesAndAdjustments lossAndAdjustment, List<LossesAndAdjustmentsType> lossesAndAdjustmentsTypes) { Predicate predicate = new Predicate() { @Override public boolean evaluate(Object o) { return lossAndAdjustment.getType().getName().equals(((LossesAndAdjustmentsType) o).getName()); } }; LossesAndAdjustmentsType lossAndAdjustmentTypeFromList = (LossesAndAdjustmentsType) find(lossesAndAdjustmentsTypes, predicate); return lossAndAdjustmentTypeFromList.getAdditive(); } }