diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy index 7533671451b..5847c81e3b4 100644 --- a/grails-app/conf/UrlMappings.groovy +++ b/grails-app/conf/UrlMappings.groovy @@ -15,6 +15,7 @@ class UrlMappings { "/admin/manage/$action?"(controller: "adminManage") "/adminManage/$action?"(controller: "errors", action: "urlMapping") + "/snapshot/$action?"(controller: "inventorySnapshot") "/$controller/$action?/$id?" { constraints { diff --git a/grails-app/jobs/org/pih/warehouse/jobs/CalculateQuantityJob.groovy b/grails-app/jobs/org/pih/warehouse/jobs/CalculateQuantityJob.groovy index b0dc23408c9..dda9a8c8d93 100755 --- a/grails-app/jobs/org/pih/warehouse/jobs/CalculateQuantityJob.groovy +++ b/grails-app/jobs/org/pih/warehouse/jobs/CalculateQuantityJob.groovy @@ -28,36 +28,35 @@ class CalculateQuantityJob { // System uses yesterday by default if a date is not provided if (!date) { - log.info "Date is being set to yesterday" + log.info "Date is being set to midnight tonight" date = new Date() - date.clearTime() + //date.clearTime() } log.info "Executing calculate quantity job for date=${date}, user=${user}, location=${location}, product=${product}, mergedJobDataMap=${context.mergedJobDataMap}" - + // Triggered ?? if (product && date && location) { println "Triggered calculate quantity job for product ${product} at ${location} on ${date}" inventoryService.createOrUpdateInventorySnapshot(date, location, product) } + // Triggered by the inventory snapshot tab off the product page else if (product && location) { println "Triggered calculate quantity job for product ${product} at ${location} on ${date}" inventoryService.createOrUpdateInventorySnapshot(location, product) } + // Triggered by the Inventory Snapshot page else if (date && location) { println "Triggered calculate quantity job for all products at ${location} on ${date}" inventoryService.createOrUpdateInventorySnapshot(date, location) inventoryService.createOrUpdateInventoryItemSnapshot(date, location) } + // Triggered by the CalculateQuantityJob else if (date) { println "Triggered calculate quantity job for all locations and products on ${date}" inventoryService.createOrUpdateInventorySnapshot(date) inventoryService.createOrUpdateInventoryItemSnapshot(date) } - else if (location) { - println "Triggered calculate quantity job for all products at location ${location} over all dates" - inventoryService.createOrUpdateInventorySnapshot(location) - } else { println "Triggered calculate quantity job for all dates, locations, products" def transactionDates = inventoryService.getTransactionDates() diff --git a/grails-app/migrations/0.6.x/add-unique-index-to-snapshot-tables.xml b/grails-app/migrations/0.6.x/add-unique-index-to-snapshot-tables.xml new file mode 100644 index 00000000000..80db9849bc0 --- /dev/null +++ b/grails-app/migrations/0.6.x/add-unique-index-to-snapshot-tables.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + diff --git a/grails-app/migrations/0.6.x/changelog.xml b/grails-app/migrations/0.6.x/changelog.xml index 26ece01602f..e5611fcb90b 100755 --- a/grails-app/migrations/0.6.x/changelog.xml +++ b/grails-app/migrations/0.6.x/changelog.xml @@ -6,5 +6,5 @@ - + diff --git a/grails-app/services/org/pih/warehouse/inventory/InventoryService.groovy b/grails-app/services/org/pih/warehouse/inventory/InventoryService.groovy index e51ccc0ea6f..ca0b909df6c 100755 --- a/grails-app/services/org/pih/warehouse/inventory/InventoryService.groovy +++ b/grails-app/services/org/pih/warehouse/inventory/InventoryService.groovy @@ -11,12 +11,14 @@ package org.pih.warehouse.inventory import grails.plugin.springcache.annotations.Cacheable import grails.validation.ValidationException +import groovy.sql.Sql import groovy.time.TimeCategory import org.apache.commons.lang.StringEscapeUtils import org.apache.commons.lang.StringUtils import org.grails.plugins.csv.CSVWriter import org.hibernate.annotations.Cache import org.hibernate.criterion.CriteriaSpecification +import org.hibernate.id.UUIDHexGenerator import org.joda.time.LocalDate import org.pih.warehouse.auth.AuthService import org.pih.warehouse.core.Constants @@ -38,6 +40,7 @@ import org.springframework.context.ApplicationContextAware import org.springframework.validation.Errors import util.InventoryUtil +import java.sql.BatchUpdateException import java.text.ParseException; import java.util.Random @@ -46,12 +49,8 @@ import java.text.SimpleDateFormat class InventoryService implements ApplicationContextAware { + def dataSource def sessionFactory - def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP - def startTime = System.currentTimeMillis() - def lastBatchStarted = startTime - - def dataService def productService def identifierService @@ -3233,7 +3232,7 @@ class InventoryService implements ApplicationContextAware { def transactionEntries = [] if (date) { def products = Tag.get(tag?.id)?.products - log.info "Products: " + products + log.info "Get products by tag ${tag?.id}: " + products transactionEntries = criteria.list { if (products) { inventoryItem { @@ -3681,15 +3680,8 @@ class InventoryService implements ApplicationContextAware { date.clearTime() def locations = getDepotLocations() locations.each { location -> - - def count = InventorySnapshot.countByDateAndLocation(date, location) - if (count == 0) { - log.info "Creating or updating inventory snapshot for date ${date}, location ${location.name} ..." - createOrUpdateInventorySnapshot(date, location) - } - else { - log.warn "Skipping inventory snapshot for date ${date}, location ${location.name} as ${count} inventory snapshots already exist" - } + log.info "Creating or updating inventory snapshot for date ${date}, location ${location.name} ..." + createOrUpdateInventorySnapshot(date, location) } println "Created inventory snapshot for ${date} in " + (System.currentTimeMillis() - startTime) + " ms" @@ -3698,30 +3690,37 @@ class InventoryService implements ApplicationContextAware { def createOrUpdateInventorySnapshot(Date date, Location location) { try { - def inventorySnapshots = InventorySnapshot.countByDateAndLocation(date, location) - log.info "Date ${date}, location ${location}: " + inventorySnapshots - //if (inventorySnapshots == 0) { - log.info "Create or update inventory snapshot for location ${location.name} on date ${date}" - //Location.withSession { - if (!location.isAttached()) { - location.attach() - } - // Only process locations with inventory - if (location?.inventory) { - //def productQuantityMap = getQuantityByProductMap(location.inventory) - def quantityMap = getQuantityOnHandAsOfDate(location, date) - def products = quantityMap.keySet(); - log.info "Saving inventory snapshot for ${products?.size()} products" - products.eachWithIndex { product, index -> - def onHandQuantity = quantityMap[product] - updateInventorySnapshot(date, product, location, onHandQuantity) - if (index % 50 == 0) { - cleanUpGorm(index) - } - } - } - //} - //} + log.info "Create or update inventory snapshot for location ${location.name} on date ${date}" + // Only process locations with inventory + if (location?.inventory) { + String dateString = date.format("yyyy-MM-dd HH:mm:ss") + //def productQuantityMap = getQuantityByProductMap(location.inventory) + def quantityMap = getQuantityOnHandAsOfDate(location, date) + def products = quantityMap.keySet(); + + log.info "Saving inventory snapshots for ${products?.size()} products " + def startTime = System.currentTimeMillis() + + def sql = new Sql(dataSource) + if (sql) { + try { + sql.withBatch(1000) { stmt -> + products.eachWithIndex { product, index -> + //log.info "Saving inventory snapshot for product[${index}]: " + product + def onHandQuantity = quantityMap[product] + def insertStmt = "insert into inventory_snapshot(version,date,location_id,product_id,inventory_item_id,quantity_on_hand) " + + "values (0,'${dateString}','${location?.id}','${product?.id}',NULL,${onHandQuantity}) " + + "ON DUPLICATE KEY UPDATE quantity_on_hand=${onHandQuantity}" + stmt.addBatch(insertStmt) + } + stmt.executeBatch() + } + } catch (BatchUpdateException e) { + log.error("Error executing batch update for location ${location.name} " + e.message, e) + } + } + log.info ("Time to execute batch statements " + (System.currentTimeMillis() - startTime) + " ms") + } log.info "Saved inventory snapshot for products=ALL, location=${location}, date=${date}" } catch (Exception e) { log.error("Unable to complete inventory snapshot process", e) @@ -3732,13 +3731,9 @@ class InventoryService implements ApplicationContextAware { try { def dates = getTransactionDates(location, product) dates.each { date -> - //def inventorySnapshots = InventorySnapshot.countByDateAndLocation(date, location) - //println "Date ${date}, location ${location}: " + inventorySnapshots - //if (inventorySnapshots == 0) { def quantity = getQuantity(product, location, date) log.info "Create or update inventory snapshot for product ${product} at location ${location.name} on date ${date} = ${quantity} ${product.unitOfMeasure}" - updateInventorySnapshot(date, product, location, quantity) - //} + createOrUpdateInventorySnapshot(date, product, location, quantity) } log.info "Saved inventory snapshot for product=${product.productCode}, location=${location}, dates=ALL" } catch (Exception e) { @@ -3755,7 +3750,7 @@ class InventoryService implements ApplicationContextAware { if (inventorySnapshots == 0) { log.info "Create or update inventory snapshot for location ${location.name} on date ${date}" def quantity = getQuantity(product, location, date) - updateInventorySnapshot(date, product, location, quantity) + createOrUpdateInventorySnapshot(date, product, location, quantity) } log.info "Saved inventory snapshot for product=${product.productCode}, location=${location}, date=${date}" } catch (Exception e) { @@ -3764,10 +3759,10 @@ class InventoryService implements ApplicationContextAware { } - def updateInventorySnapshot(Date date, Product product, Location location, Integer onHandQuantity) { - //log.info "Updating inventory snapshot for product " + product.name + " @ " + location.name + def createOrUpdateInventorySnapshot(Date date, Product product, Location location, Integer onHandQuantity) { + log.info "Updating inventory snapshot for product " + product.name + " @ " + location.name try { - def inventorySnapshot = InventorySnapshot.findWhere(date: date, location: location, product:product) + def inventorySnapshot = InventorySnapshot.findWhere(date: date, location: location, product:product) if (!inventorySnapshot) { inventorySnapshot = new InventorySnapshot(date: date, location: location, product: product) } @@ -3776,7 +3771,7 @@ class InventoryService implements ApplicationContextAware { //inventorySnapshot.quantityInbound = pendingQuantity[0]?:0 //inventorySnapshot.quantityOutbound = pendingQuantity[1]?:0 //inventorySnapshot.lastUpdated = new Date() - inventorySnapshot.save() + inventorySnapshot.save(flush:true) } catch (Exception e) { log.error("Error saving inventory snapshot for product " + product.name + " and location " + location.name, e) @@ -3785,47 +3780,55 @@ class InventoryService implements ApplicationContextAware { } + + def createOrUpdateInventoryItemSnapshot(Date date) { def startTime = System.currentTimeMillis() date.clearTime() def locations = getDepotLocations() locations.each { location -> - - def count = InventoryItemSnapshot.countByDateAndLocation(date, location) - if (count == 0) { - log.info "Creating or updating inventory item snapshot for date ${date} and location ${location.name} ..." - createOrUpdateInventoryItemSnapshot(date, location) - } - else { - log.warn "Skipping inventory item snapshot for date ${date} and location ${location.name} as ${count} inventory item snapshots already exist" - } + log.info "Creating or updating inventory item snapshot for date ${date} and location ${location.name} ..." + createOrUpdateInventoryItemSnapshot(date, location) } println "Created inventory snapshot for ${date} in " + (System.currentTimeMillis() - startTime) + " ms" } + + def createOrUpdateInventoryItemSnapshot(Date date, Location location) { + try { - if (!location.isAttached()) { - location.attach() - } def inventoryItemSnapshots = InventoryItemSnapshot.countByDateAndLocation(date, location) log.info "Date ${date}, location ${location}: " + inventoryItemSnapshots log.info "Create or update inventory snapshot for location ${location.name} on date ${date}" // Only process locations with inventory if (location.inventory) { - def onHandQuantityMap = getQuantityForInventory(location.inventory) - def inventoryItems = onHandQuantityMap.keySet(); - - log.info "Saving inventory snapshot for ${inventoryItems?.size()} inventory items" + // Get quantity on hand for all products at a given location + def onHandQuantityMap = getQuantityForInventory(location.inventory) + def inventoryItems = onHandQuantityMap.keySet(); + log.info "Saving inventory item snapshots for ${inventoryItems?.size()} inventory items" + + def startTime = System.currentTimeMillis() + def sql = new Sql(dataSource) + if (sql) { + String dateString = date.format("yyyy-MM-dd HH:mm:ss") + sql.withBatch(1000) { stmt -> + inventoryItems.eachWithIndex { inventoryItem, index -> + //log.info "Saving inventory snapshot for product[${index}]: " + product + def onHandQuantity = onHandQuantityMap[inventoryItem] + + //stmt.addBatch(date:date.format("yyyy-MM-dd hh:mm:ss"), locationId:location.id, productId:product.id, inventoryItemId:null, quantityOnHand:onHandQuantity) + def insertStmt = "insert into inventory_snapshot(id,version,date,location_id,product_id,inventory_item_id,quantity_on_hand,date_created,last_updated) " + + "values ('${UUID.randomUUID().toString()}', 0,'${dateString}','${location?.id}','${inventoryItem?.product?.id}',NULL,${onHandQuantity},now(),now()) " + + "ON DUPLICATE KEY UPDATE quantity_on_hand=${onHandQuantity},last_updated=now()" + stmt.addBatch(insertStmt) + } + stmt.executeBatch() + } + } + log.info ("Time to execute batch statements " + (System.currentTimeMillis() - startTime) + " ms") - inventoryItems.eachWithIndex { inventoryItem, index -> - def onHandQuantity = onHandQuantityMap[inventoryItem] - updateInventoryItemSnapshot(date, inventoryItem, location, onHandQuantity) - if (index % 50 == 0) { - cleanUpGorm(index) - } - } } log.info "Saved inventory snapshot for all products at location=${location.name} on date=${date}" } catch (Exception e) { @@ -3833,8 +3836,8 @@ class InventoryService implements ApplicationContextAware { } } - def updateInventoryItemSnapshot(Date date, InventoryItem inventoryItem, Location location, Integer quantityOnHand) { - //log.info "Updating inventory snapshot for product " + product.name + " @ " + location.name + def createOrUpdateInventoryItemSnapshot(Date date, InventoryItem inventoryItem, Location location, Integer quantityOnHand) { + log.info "Updating inventory snapshot for product " + inventoryItem?.product?.name + " at location " + location.name try { def inventoryItemSnapshot = InventoryItemSnapshot.findWhere(date: date, location: location, product:inventoryItem?.product, inventoryItem: inventoryItem) if (!inventoryItemSnapshot) { @@ -3853,8 +3856,13 @@ class InventoryService implements ApplicationContextAware { } } - - + /** + * Calculate pending quantity for a given product and location. + * + * @param product + * @param location + * @return + */ def calculatePendingQuantity(product, location) { def inboundQuantity = 0; def outboundQuantity = 0; @@ -3891,25 +3899,12 @@ class InventoryService implements ApplicationContextAware { [inboundQuantity, outboundQuantity] } - def cleanUpGorm(index) { - def session = sessionFactory.currentSession - session.flush() - session.clear() - propertyInstanceMap.get().clear() - printStatus(index) - } - - def printStatus(index) { - def batchEnded = System.currentTimeMillis() - def milliseconds = (batchEnded-lastBatchStarted) - //def total = (batchEnded-startTime)/1000 - log.info "Flushed last batch ${index} ... took ${milliseconds} ms" - lastBatchStarted = batchEnded - } - - - - + /** + * Build quantity map for all products at a given location. + * + * @param location + * @return + */ def getQuantityMap(Location location) { def quantityMap = [:] def products = Product.list() @@ -3919,12 +3914,23 @@ class InventoryService implements ApplicationContextAware { return quantityMap } + /** + * Calculate the quantity on hand for a given inventory item and location. + * + * @param inventoryItem + * @param location + */ def calculateQuantityOnHand(InventoryItem inventoryItem, Location location) { throw new UnsupportedOperationException("Method has not been implemented yet") } - - + /** + * Calculate the quantity on hand for a given product and location. + * + * @param product + * @param location + * @return + */ def calculateQuantityOnHand(Product product, Location location) { long startTime = System.currentTimeMillis() @@ -3949,7 +3955,13 @@ class InventoryService implements ApplicationContextAware { return quantityOnHand - quantityDebit + quantityCredit } - + /** + * Get most recent product inventory transaction. + * + * @param product + * @param location + * @return + */ def getMostRecentQuantityOnHand(Product product, Location location) { def results = Transaction.executeQuery( """ SELECT max(te.transaction.transactionDate), sum(te.quantity) diff --git a/grails-app/views/inventorySnapshot/_sidebar.gsp b/grails-app/views/inventorySnapshot/_sidebar.gsp index a05933a78e2..f6b035a9a35 100644 --- a/grails-app/views/inventorySnapshot/_sidebar.gsp +++ b/grails-app/views/inventorySnapshot/_sidebar.gsp @@ -61,16 +61,18 @@

Re-indexing

If data is stale or does not exist, you can run a background process that re-indexes the quantity on hand values for the current location and selected date.

-
-
- - +
+ +
+ + +
diff --git a/grails-app/views/inventorySnapshot/list.gsp b/grails-app/views/inventorySnapshot/list.gsp index 9bf0e0fce21..03d128ba12a 100644 --- a/grails-app/views/inventorySnapshot/list.gsp +++ b/grails-app/views/inventorySnapshot/list.gsp @@ -21,8 +21,8 @@ <%-- - Date - Location--%> + Date--%> + Location SKU Product Product group @@ -154,7 +154,7 @@ ], "aoColumns": [ // { "mData": "date" }, // 0 - // { "mData": "location" }, // 1 + { "mData": "location" }, // 1 { "mData": "productCode" }, // 2 { "mData": "product" }, // 2 { "mData": "productGroup" }, // 2