@@ -11,13 +11,23 @@ package org.pih.warehouse.core
1111
1212import grails.gorm.transactions.Transactional
1313import grails.plugins.csv.CSVWriter
14+ import grails.util.Holders
15+ import org.grails.plugins.web.taglib.ApplicationTagLib
1416import org.hibernate.sql.JoinType
1517
1618import org.pih.warehouse.DateUtil
19+ import org.pih.warehouse.auth.AuthService
20+ import org.pih.warehouse.core.localization.MessageLocalizer
21+ import org.pih.warehouse.forecasting.ForecastingService
22+ import org.pih.warehouse.importer.CSVUtils
1723import org.pih.warehouse.inventory.InventoryItem
1824import org.pih.warehouse.inventory.InventoryLevel
25+ import org.pih.warehouse.inventory.InventoryLevelStatus
1926import org.pih.warehouse.inventory.InventoryStatus
27+ import org.pih.warehouse.inventory.ReorderReportFilterCommand
28+ import org.pih.warehouse.inventory.ReorderReportItemDto
2029import org.pih.warehouse.inventory.TransactionEntry
30+ import org.pih.warehouse.inventory.product.availability.InventoryByProduct
2131import org.pih.warehouse.product.Category
2232import org.pih.warehouse.product.Product
2333import org.pih.warehouse.report.InventoryReportCommand
@@ -31,6 +41,9 @@ class DashboardService {
3141
3242ConfigService configService
3343def productAvailabilityService
44+ ForecastingService forecastingService
45+ UserService userService
46+ MessageLocalizer messageLocalizer
3447
3548/**
3649 * Get fast moving items based on requisition data.
@@ -573,6 +586,129 @@ class DashboardService {
573586return reorderStock
574587 }
575588
589+ List<ReorderReportItemDto > getReorderReport (ReorderReportFilterCommand command ) {
590+ // Find inventory levels that don't have bin location set and have AT LEAST one of min/max qty set
591+ List<InventoryLevel > inventoryLevels= InventoryLevel . createCriteria(). list {
592+ // Current location is a "central" one, we want to get inventory level values (max/min qty) only from the current location
593+ // additionaLocations are supposed to be used only for the quantity available to promise calculation,
594+ // where we sum the QATP for currentLocation + additionalLocations
595+ eq(" inventory" ,AuthService . currentLocation. inventory)
596+ isNull(" internalLocation" )
597+ or {
598+ and {
599+ isNotNull(" minQuantity" )
600+ gt(" minQuantity" ,0 )
601+ }
602+ and {
603+ isNotNull(" reorderQuantity" )
604+ gt(" reorderQuantity" ,0 )
605+ }
606+ }
607+ }
608+ List<InventoryByProduct > inventoriesByProduct= productAvailabilityService. getInventoriesByProduct(command. additionalLocations,
609+ command. categories,
610+ command. tags,
611+ command. expiration)
612+
613+ // Build a map of inventory levels keyed on product to have O(1) read access in the .findResults below instead of O(n) if we were to do .find on List
614+ Map<Product ,InventoryLevel > inventoryLevelByProduct= inventoryLevels. collectEntries { [it. product, it] }
615+
616+ // .findResults is a shortened way of doing .findAll{...}.collect{...}
617+ List<ReorderReportItemDto > reorderReportItems= inventoriesByProduct. findResults {InventoryByProduct item ->
618+ InventoryLevel inventoryLevel= inventoryLevelByProduct[item. product]
619+
620+ // Depending on the provided inventory level status filter value, we have a different condition for filtering out the items
621+ Map<InventoryLevelStatus ,Closure<Boolean > > inventoryLevelStatusFilterCondition= [
622+ (InventoryLevelStatus . IN_STOCK ): { item. quantityAvailableToPromise> 0 },
623+ (InventoryLevelStatus . BELOW_MAXIMUM ): { inventoryLevel?. maxQuantity&& item. quantityAvailableToPromise<= inventoryLevel?. maxQuantity },
624+ (InventoryLevelStatus . BELOW_REORDER ): { inventoryLevel?. reorderQuantity&& item. quantityAvailableToPromise<= inventoryLevel?. reorderQuantity },
625+ (InventoryLevelStatus . BELOW_MINIMUM ): { inventoryLevel?. minQuantity&& item. quantityAvailableToPromise<= inventoryLevel?. minQuantity }
626+ ]
627+
628+ // If an item satisfies the predicate, call the buildReorderReportItem, otherwise return null so that .findResults filters out the record in the end
629+ return inventoryLevelStatusFilterCondition[command. inventoryLevelStatus](item)? buildReorderReportItem(item, inventoryLevel): null
630+ }
631+
632+ return reorderReportItems
633+ }
634+
635+ private ReorderReportItemDto buildReorderReportItem (InventoryByProduct item ,InventoryLevel inventoryLevel ) {
636+ Map<String ,Number > monthlyDemand= forecastingService. getDemand(AuthService . currentLocation,null , item. product)
637+ Integer quantityToOrder= inventoryLevel?. maxQuantity!= null ? inventoryLevel. maxQuantity- item. quantityAvailableToPromise: null
638+ Boolean hasRoleFinance= userService. hasRoleFinance(AuthService . currentUser)
639+ BigDecimal unitCost= hasRoleFinance? item. product. pricePerUnit: null
640+ BigDecimal expectedReorderCost= hasRoleFinance&& quantityToOrder&& item. product. pricePerUnit!= null
641+ ? quantityToOrder* unitCost
642+ :null
643+ Closure determineInventoryStatus= {InventoryLevel inventoryLevel1 ,Integer quantityOnHand ->
644+ if (inventoryLevel1) {
645+ return inventoryLevel1. statusMessage(quantityOnHand)
646+ }
647+ return quantityOnHand> 0 ? " IN_STOCK" : " STOCK_OUT"
648+ }
649+ return new ReorderReportItemDto (
650+ inventoryStatus : determineInventoryStatus(inventoryLevel, item. quantityOnHand),
651+ product : item. product,
652+ tags : item. product. tags,
653+ inventoryLevel : inventoryLevel,
654+ monthlyDemand : monthlyDemand?. monthlyDemand?: 0 ,
655+ quantityAvailableToPromise : item. quantityAvailableToPromise,
656+ quantityToOrder : quantityToOrder,
657+ unitCost : unitCost,
658+ expectedReorderCost : expectedReorderCost
659+ )
660+ }
661+
662+ String getReorderReportCsv (List<ReorderReportItemDto > reorderReport ) {
663+ StringWriter sw= new StringWriter ()
664+ Boolean hasRoleFinance= userService. hasRoleFinance(AuthService . currentUser)
665+ CSVWriter csv= new CSVWriter (sw, {
666+ " ${ messageLocalizer.localize("inventoryLevel.status.label")} " { it?. status }
667+ " ${ messageLocalizer.localize("product.productCode.label")} " { it?. productCode }
668+ " ${ messageLocalizer.localize("product.label")} " { it?. product }
669+ " ${ messageLocalizer.localize("category.label")} " { it?. category }
670+ " ${ messageLocalizer.localize("product.tags.label", "Tags")} " { it?. tags }
671+ " ${ messageLocalizer.localize("product.unitOfMeasure.label", "Unit of measure")} " { it?. unitOfMeasure }
672+ " ${ messageLocalizer.localize("product.vendor.label", "Vendor")} " { it?. vendor }
673+ " ${ messageLocalizer.localize("product.vendorCode.label", "Vendor code")} " { it?. vendorCode }
674+ " ${ messageLocalizer.localize("inventoryLevel.minQuantity.label", "Min quantity")} " { it?. minQuantity }
675+ " ${ messageLocalizer.localize("inventoryLevel.reorderQuantity.label", "Reorder quantity")} " { it?. reorderQuantity }
676+ " ${ messageLocalizer.localize("inventoryLevel.maxQuantity.label", "Max quantity")} " { it?. maxQuantity }
677+ " ${ messageLocalizer.localize("report.averageMonthlyDemand.label", "Average Monthly Demand")} " { it?. averageMonthlyDemand }
678+ " ${ messageLocalizer.localize("product.quantityAvailableToPromise.label", "Quantity Available")} " { it?. quantityAvailableToPromise }
679+ " ${ messageLocalizer.localize("inventory.quantityToOrder.message", "Quantity to Order")} " { it?. quantityToOrder }
680+
681+ if (hasRoleFinance) {
682+ " ${ messageLocalizer.localize("product.unitCost.label", "Unit Cost")} " { it?. unitCost }
683+ " ${ messageLocalizer.localize("inventory.expectedReorderCost.label", "Expected Reorder Cost")} " { it?. expectedReorderCost }
684+ }
685+ })
686+ Closure getQuantityToOrderDisplayValue= {Integer maxQuantity ,Integer quantityToOrder ->
687+ return maxQuantity== null ? messageLocalizer. localize(" reorderReport.noMaxQtySet.label" ): quantityToOrder
688+ }
689+ reorderReport. each {ReorderReportItemDto item ->
690+ csv<< [
691+ status :" ${ messageLocalizer.localize("enum.InventoryLevelStatusCsv." + item.inventoryStatus)} " ,
692+ productCode : item. product. productCode,
693+ product : item. product. displayNameOrDefaultName,
694+ category : item. product. category?. name?: " " ,
695+ tags : item. product. tagsToString(),
696+ unitOfMeasure : item. product. unitOfMeasure?: " " ,
697+ vendor : item. product. vendor?: " " ,
698+ vendorCode : item. product. vendorCode?: " " ,
699+ minQuantity : item. inventoryLevel?. minQuantity?: " " ,
700+ reorderQuantity : item. inventoryLevel?. reorderQuantity?: " " ,
701+ maxQuantity : item. inventoryLevel?. maxQuantity?: " " ,
702+ averageMonthlyDemand : item. monthlyDemand?: 0 ,
703+ quantityAvailableToPromise : item. quantityAvailableToPromise,
704+ quantityToOrder : getQuantityToOrderDisplayValue(item. inventoryLevel?. maxQuantity, item. quantityToOrder)?: " " ,
705+ unitCost : item. unitCost?: " " ,
706+ expectedReorderCost : item. expectedReorderCost?: " " ,
707+ ]
708+ }
709+ return CSVUtils . prependBomToCsvString(csv. writer. toString())
710+ }
711+
576712def getReorderReport (Location location ) {
577713long startTime= System . currentTimeMillis()
578714ArrayList<Map > inventoryItems= getInventoryItems(location)