Index: trunk/grails-app/conf/Config.groovy
===================================================================
--- trunk/grails-app/conf/Config.groovy	(revision 645)
+++ trunk/grails-app/conf/Config.groovy	(revision 646)
@@ -209,5 +209,6 @@
             [order:20, controller:'inventoryItemDetailed', title:'Create', action:'create', isVisible: { true }],
             [order:90, controller:'inventoryItemDetailed', title:'Show', action:'show', isVisible: { params.action == 'show' }],
-            [order:91, controller:'inventoryItemDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }]
+            [order:91, controller:'inventoryItemDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }],
+            [order:91, controller:'inventoryItemDetailed', title:'Reorder', action:'reorder', isVisible: { params.action == 'reorder' }]
         ]
     ],
Index: trunk/grails-app/controllers/InventoryItemDetailedController.groovy
===================================================================
--- trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 645)
+++ trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 646)
@@ -34,4 +34,20 @@
         }
         forward(action: 'search', params: params)
+    }
+
+    /**
+    * Set session.inventoryItemReorderSearchParamsMax
+    */
+    @Secured(['ROLE_AppAdmin', 'ROLE_Manager', 'ROLE_InventoryManager', 'ROLE_InventoryUser'])
+    def setReorderSearchParamsMax = {
+        def max = 1000
+        if(params.newMax?.isInteger()) {
+            def i = params.newMax.toInteger()
+            if(i > 0 && i <= max)
+                session.inventoryItemReorderSearchParamsMax = params.newMax
+            if(i > max)
+                session.inventoryItemReorderSearchParamsMax = max
+        }
+        forward(action: 'reorder', params: params)
     }
 
@@ -542,3 +558,154 @@
     }
 
+    /**
+    * Search for Inventory items that require reorder.
+    */
+    @Secured(['ROLE_AppAdmin', 'ROLE_Manager', 'ROLE_InventoryManager'])
+    def reorder = {
+
+        // In the case of an actionSubmit button, rewrite action name from 'index'.
+        if(params._action_reorder)
+            params.action='reorder'
+
+        if(session.inventoryItemReorderSearchParamsMax)
+            params.max = session.inventoryItemReorderSearchParamsMax
+
+        def inventoryItemInstanceList = []
+        def inventoryItemInstanceTotal
+        def filterParams = params
+        def suppliers = Supplier.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }
+        def inventoryGroups = InventoryGroup.findAllByIsActive(true)
+        if(!params.selectedGroups)
+            params.selectedGroups = inventoryGroups.collect{it.id}
+        else
+            params.selectedGroups = params.selectedGroups.collect { it.toLong() }
+
+        // Restore search unless a new search is being requested.
+        if(!params.newSearch && !params.quickSearch) {
+            if(session.reorderSearchSelectedGroups) {
+                params.selectedGroups = session.reorderSearchSelectedGroups
+                params.selectedSupplier = session.reorderSearchSelectedSupplier
+                params.includeAlternateSuppliers = session.reorderSearchIncludeAlternateSuppliers
+                params.includeReorderListingDisabled = session.reorderSearchIncludeReorderListingDisabled
+                params.includeOnBackOrder = session.reorderSearchIncludeOnBackOrder
+            }
+            else if(session.inventoryItemReorderQuickSearch) {
+                params.quickSearch = session.inventoryItemReorderQuickSearch
+                if(session.inventoryItemReorderQuickSearchDaysBack)
+                    params.daysBack = session.inventoryItemReorderQuickSearchDaysBack.toString()
+            }
+        }
+
+        // Remember sort if supplied, otherwise try to restore.
+        if(params.sort && params.order) {
+             session.inventoryItemReorderSearchSort = params.sort
+             session.inventoryItemReorderSearchOrder = params.order
+        }
+        else if(session.inventoryItemReorderSearchSort && session.inventoryItemReorderSearchOrder) {
+            params.sort = session.inventoryItemReorderSearchSort
+            params.order = session.inventoryItemReorderSearchOrder
+        }
+
+        if(params.quickSearch) {
+            // Quick Search Links:
+            if(!params.quickSearch) params.quickSearch = "inventoryBelowReorder"
+            def result = inventoryItemSearchService.getQuickSearch(params, RCU.getLocale(request))
+            inventoryItemInstanceList = result.inventoryItemList
+            inventoryItemInstanceTotal = result.inventoryItemList.totalCount
+            params.message = result.message
+            filterParams.quickSearch = result.quickSearch
+            // Remember search.
+            session.inventoryItemReorderQuickSearch = result.quickSearch
+            if(result.daysBack)
+                session.inventoryItemReorderQuickSearchDaysBack = result.daysBack
+            // Clear any previous search.
+            session.removeAttribute("reorderSearchSelectedGroups")
+            session.removeAttribute("reorderSearchSelectedSupplier")
+            session.removeAttribute("reorderSearchIncludeAlternateSuppliers")
+            session.removeAttribute("reorderSearchIncludeReorderListingDisabled")
+            session.removeAttribute("reorderSearchIncludeOnBackOrder")
+        }
+        else {
+            // Reorder Search:
+            def result = inventoryItemSearchService.getReorderSearch(params, RCU.getLocale(request))
+            inventoryItemInstanceList = result.inventoryItemList
+            inventoryItemInstanceTotal = result.inventoryItemList.totalCount
+            params.message = result.message
+            // Place limit search selects in filterParams for pagination.
+            if(params.selectedGroups) {
+                filterParams.selectedGroups = params.selectedGroups
+                filterParams.selectedSupplier = params.selectedSupplier
+                filterParams.includeAlternateSuppliers = params.includeAlternateSuppliers
+                filterParams.includeReorderListingDisabled = params.includeReorderListingDisabled
+                filterParams.includeOnBackOrder = params.includeOnBackOrder
+            }
+            // Remember search.
+            session.reorderSearchSelectedGroups = params.selectedGroups
+            session.reorderSearchSelectedSupplier = params.selectedSupplier
+            session.reorderSearchIncludeAlternateSuppliers = params.includeAlternateSuppliers
+            session.reorderSearchIncludeReorderListingDisabled = params.includeReorderListingDisabled
+            session.reorderSearchIncludeOnBackOrder = params.includeOnBackOrder
+            // Clear any previous search.
+            session.removeAttribute("inventoryItemReorderQuickSearch")
+            session.removeAttribute("inventoryItemReorderQuickSearchDaysBack")
+        }
+
+        // export plugin:
+        if(params?.format && params.format != "html") {
+
+            def dateFmt = { date ->
+                formatDate(format: "EEE, dd-MMM-yyyy", date: date)
+            }
+
+            String title
+            if(params.quickSearch)
+                title = params.message
+            else
+                title = "Filtered Inventory List."
+
+            response.contentType = ConfigurationHolder.config.grails.mime.types[params.format]
+            response.setHeader("Content-disposition", "attachment; filename=Inventory.${params.extension}")
+            List fields = ["name",
+                                "description",
+                                "inventoryGroup",
+                                "unitsInStock",
+                                "reorderPoint",
+                                "unitOfMeasure",
+                                "inventoryLocation",
+                                "inventoryLocation.inventoryStore"]
+            Map labels = ["name": "Name",
+                                "description": "Description",
+                                "inventoryGroup": "Group",
+                                "unitsInStock":"In Stock",
+                                "reorderPoint":"Reorder Point",
+                                "unitOfMeasure": "UOM",
+                                "inventoryLocation": "Location",
+                                "inventoryLocation.inventoryStore": "Store"]
+
+            Map formatters = [:]
+            Map parameters = [title: title, separator: ","]
+
+            exportService.export(params.format,
+                                                response.outputStream,
+                                                inventoryItemInstanceList.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) },
+                                                fields,
+                                                labels,
+                                                formatters,
+                                                parameters)
+        }
+
+        // Add some basic params to filterParams.
+        filterParams.max = params.max
+        filterParams.offset = params.offset?.toInteger() ?: 0
+        filterParams.sort = params.sort ?: "name"
+        filterParams.order = params.order ?: "asc"
+
+        return[ inventoryItemInstanceList: inventoryItemInstanceList,
+                        inventoryItemInstanceTotal: inventoryItemInstanceTotal,
+                        filterParams: filterParams,
+                        params: params,
+                        inventoryGroups: inventoryGroups,
+                        suppliers: suppliers]
+    } // end reorder()
+
 } // end of class
Index: trunk/grails-app/services/InventoryItemSearchService.groovy
===================================================================
--- trunk/grails-app/services/InventoryItemSearchService.groovy	(revision 645)
+++ trunk/grails-app/services/InventoryItemSearchService.groovy	(revision 646)
@@ -1,2 +1,3 @@
+import net.kromhouts.HqlBuilder
 import grails.orm.PagedResultList
 import org.compass.core.engine.SearchEngineQueryParseException
@@ -104,4 +105,80 @@
             } // createCriteria
     } // getInventoryBelowReorder
+
+    /**
+    * Search for inventory items that are below reorder point.
+    * @param params The request params.
+    * @param locale The locale to use when generating result.message.
+    */
+    def getReorderSearch(params, locale) {
+        def result = [:]
+
+        def getMessage = { Map m ->
+            messageSource.getMessage(m.code, m.args == null ? null : m.args.toArray(), locale)
+        }
+
+        params.max = Math.min(params?.max?.toInteger() ?: 10, paramsMax)
+        params.offset = params?.offset?.toInteger() ?: 0
+
+        def sort = "inventoryItem." + (params?.sort ?: "name")
+        def order = params?.order == "desc" ? "desc" : "asc"
+
+        def q = new HqlBuilder(max: params.max, offset: params.offset).query {
+            select 'count(distinct inventoryItem) as inventoryItemCount'
+            from 'InventoryItem as inventoryItem',
+                    'left join inventoryItem.alternateSuppliers as alternateSupplier'
+            where 'inventoryItem.unitsInStock <= inventoryItem.reorderPoint'
+                    and 'inventoryItem.isActive = true'
+                    and 'inventoryItem.isObsolete = false'
+
+                    if(!params.includeReorderListingDisabled)
+                        and "inventoryItem.enableReorderListing = true"
+
+                    if(params.selectedSupplier?.isLong()) {
+                        namedParams.supplier = Supplier.get(params.selectedSupplier.toLong())
+                        if(params.includeAlternateSuppliers)
+                            and "(inventoryItem.preferredSupplier = :supplier or alternateSupplier = :supplier)"
+                        else
+                            and "inventoryItem.preferredSupplier = :supplier"
+                    } // if selectedSupplier
+
+                    if(params.selectedGroups) {
+                        namedParams.selectedGroupIds = params.selectedGroups
+                        and  "inventoryItem.inventoryGroup.id in(:selectedGroupIds)"
+                    }
+
+                    if(!params.includeOnBackOrder) {
+                        // Sub query!
+                        def onBackOrder = new HqlBuilder().query {
+                            from "InventoryItemPurchase p"
+                            where "p.inventoryItem = inventoryItem"
+                                    and "p.inventoryItem = inventoryItem"
+                                    and "p.inventoryItemPurchaseType.id = 1" // Order Placed.
+                                    and "p.receivedComplete = false"
+                        }
+
+                        and "not exists ($onBackOrder.query)"
+                    }
+
+        } // query
+
+        def totalCount = InventoryItem.executeQuery(q.query, q.namedParams)[0].toInteger()
+
+        q.select = 'distinct inventoryItem'
+        q.order = "by $sort $order, inventoryItem.id asc"
+        def list = InventoryItem.executeQuery(q.query, q.namedParams, q.paginateParams)
+
+        result.inventoryItemList = new PagedResultList(list, totalCount)
+
+        // Get the result message.
+        if(result.inventoryItemList.totalCount > 0)
+            result.message = getMessage(code:"inventoryItem.search.text.below.reorder.description")
+        else
+            result.message = getMessage(code:"inventoryItem.search.text.below.reorder.none.found")
+
+        // Success.
+        return result
+
+    } // getReorderSearch
 
     /**
Index: trunk/grails-app/views/inventoryItemDetailed/reorder.gsp
===================================================================
--- trunk/grails-app/views/inventoryItemDetailed/reorder.gsp	(revision 646)
+++ trunk/grails-app/views/inventoryItemDetailed/reorder.gsp	(revision 646)
@@ -0,0 +1,307 @@
+
+
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+        <meta name="layout" content="main" />
+        <title>InventoryItem Reorder</title>
+        <nav:resources override="true"/>
+        <g:render template="/shared/pictureHead" />
+        <filterpane:includes />
+        <export:resource />
+    </head>
+    <body onload="if(document.textSearchForm) {document.textSearchForm.searchText.focus();}">
+        <div class="nav">
+            <nav:renderSubItems group="nav"/>
+        </div>
+        <div class="body">
+            <g:render template="/shared/messages" />
+
+            <div class="textSearchWrapper">
+                <div class="textSearchInput">
+
+                    <g:form method="post" id="textSearchForm" name="textSearchForm" >
+                        <g:hiddenField name="newSearch" value="true" />
+
+                            <span> <!--Prevent IE inheriting margin-->
+
+                                <table>
+                                    <tr>
+                                        <td>
+                                            <label for="selectedGroups">
+                                                Group:
+                                            </label>
+                                        </td>
+                                        <td>
+                                            <custom:checkBoxList name="selectedGroups"
+                                                                            from="${inventoryGroups}"
+                                                                            value="${params.selectedGroups}"
+                                                                            optionKey="id"
+                                                                            sortBy="name"
+                                                                            height="80px"/>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>
+                                            <label for="selectedSupplier">
+                                                Supplier:
+                                            </label>
+                                        </td>
+                                        <td>
+                                            <g:select optionKey="id"
+                                                                from="${suppliers}"
+                                                                name="selectedSupplier"
+                                                                value="${params.selectedSupplier}"
+                                                                noSelection="['null':'--None--']">
+                                            </g:select>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>
+                                        </td>
+                                        <td>
+                                            <g:checkBox name="includeAlternateSuppliers" value="${params.includeAlternateSuppliers}" >
+                                            </g:checkBox>
+                                            <label for="includeAlternateSuppliers">
+                                                Include alternate suppliers
+                                            </label>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>
+                                        </td>
+                                        <td>
+                                            <g:checkBox name="includeReorderListingDisabled" value="${params.includeReorderListingDisabled}" >
+                                            </g:checkBox>
+                                            <label for="includeReorderListingDisabled">
+                                                Include reorder listing disabled
+                                            </label>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>
+                                        </td>
+                                        <td>
+                                            <g:checkBox name="includeOnBackOrder" value="${params.includeOnBackOrder}" >
+                                            </g:checkBox>
+                                            <label for="includeOnBackOrder">
+                                                Include on back order
+                                            </label>
+                                        </td>
+                                    </tr>
+                                </table>
+
+                                <div class="paginateButtons">
+                                    <span class="buttons">
+                                        <g:actionSubmit class="search" value="Search" action="reorder" />
+                                    </span>
+                                    <div class="paginateButtons">
+                                        Results: ${inventoryItemInstanceList.size()} / ${inventoryItemInstanceTotal}
+                                    </div>
+                                </div><!--paginateButtons-->
+                            </span> <!--Prevent IE inheriting margin-->
+
+                    </g:form>
+
+                </div><!--textSearchInput-->
+
+                <div class="textSearchRightFloat">
+                    <div class="paginateButtons">
+                        <span class="searchButtons">
+                            <a href='' onclick="showElement('searchPane'); return false;">Quick</a>
+                        </span>
+                        <br />
+                        <br />
+                    </div>
+
+                    <jsUtil:toggleControl toggleId="options"
+                                                            imageId="optionsImg"
+                                                            closedImgUrl="${resource(dir:'images/skin',file:'bullet_arrow_right.png')}"
+                                                            openImgUrl="${resource(dir:'images/skin',file:'bullet_arrow_down.png')}"
+                                                            text="${g.message(code: 'default.options.text')}"
+                                                            />
+                </div><!--textSearchRightFloat-->
+            </div><!--textSearchWrapper-->
+
+            <div id="options" style="display:none; clear:both;">
+                <g:form method="post" action="setSearchParamsMax" >
+                    <g:hiddenField name="params" value="${filterParams}" />
+                    <div class="dialog">
+                        <table>
+                            <tbody>
+
+                                <tr class="prop">
+                                    <td valign="top" class="name">
+                                        <label for="max">Results per page:</label>
+                                    </td>
+                                    <td valign="top" class="value">
+                                        <input type="text" maxlength="4" id="description" name="newMax" value="${params.max}"/>
+
+                                        <span class="buttons">
+                                            <g:actionSubmit action="setReorderSearchParamsMax" class="go" value="Update" />
+                                        </span>
+                                    </td>
+                                </tr>
+
+                            </tbody>
+                        </table>
+                    </div>
+                </g:form>
+                <export:formats  params="${filterParams}" formats="['csv', 'excel', 'pdf', 'rtf']"/>
+            </div>
+
+            <br />
+
+            <g:if test="${inventoryItemInstanceList.size() > 2}">
+                <g:if test="${inventoryItemInstanceTotal > inventoryItemInstanceList.size()}">
+                    <div class="paginateButtons">
+                        <g:paginate action="reorder" total="${inventoryItemInstanceTotal}" params="${filterParams}" />
+                    </div>
+                </g:if>
+            </g:if>
+
+            <g:if test="${inventoryItemInstanceList.size() > 0}">
+                <div class="list">
+                    <table>
+                        <thead>
+                            <tr>
+
+                                <th>Picture</th>
+                                <th>Description</th>
+                            
+                                <g:sortableColumn property="inventoryGroup" title="Group" params="${filterParams}" />
+                            
+                                <g:sortableColumn property="unitsInStock" title="In Stock" params="${filterParams}" />
+
+                                <th></th>
+                            
+                            </tr>
+                        </thead>
+                        <tbody>
+                        <g:each in="${inventoryItemInstanceList}" status="i" var="inventoryItemInstance">
+                            <tr class="${(i % 2) == 0 ? 'clickableOdd' : 'clickableEven'}" />
+
+                                <td class='notClickable'>
+                                    <g:if test="${inventoryItemInstance.picture}" >
+                                        <wa:pictureLightboxAnchor picture="${inventoryItemInstance.picture}"
+                                                                                            size="${Image.Small}"
+                                                                                            lightboxSize="${Image.Large}"
+                                                                                            target="_blank"
+                                                                                            title="Show Original" />
+                                    </g:if>
+                                </td>
+                            
+                                <td onclick='window.location = "${request.getContextPath()}/inventoryItemDetailed/show/${inventoryItemInstance.id}"' >
+                                    <b>${fieldValue(bean:inventoryItemInstance, field:'name')}</b><br />
+                                    ${fieldValue(bean:inventoryItemInstance, field:'description')}
+                                    <br />
+                                    <br />
+                                    Comment:<br />
+                                    ${inventoryItemInstance.comment?.encodeAsHTML()}
+                                    <br />
+                                    <br />
+                                    Reorder Point: ${fieldValue(bean:inventoryItemInstance, field:'reorderPoint')}
+                                    ${fieldValue(bean:inventoryItemInstance, field:'unitOfMeasure')}
+                                    <br />
+                                    P.Supplier: ${fieldValue(bean:inventoryItemInstance, field:'preferredSupplier')}
+                                </td>
+                            
+                                <td onclick='window.location = "${request.getContextPath()}/inventoryItemDetailed/show/${inventoryItemInstance.id}"' >
+                                    ${fieldValue(bean:inventoryItemInstance, field:'inventoryGroup')}
+                                </td>
+                            
+                                <td onclick='window.location = "${request.getContextPath()}/inventoryItemDetailed/show/${inventoryItemInstance.id}"' >
+                                    ${fieldValue(bean:inventoryItemInstance, field:'unitsInStock')}
+                                    ${fieldValue(bean:inventoryItemInstance, field:'unitOfMeasure')}
+                                </td>
+
+                                <td class="notClickable">
+                                    <g:link action="show" id="${inventoryItemInstance.id}">
+                                        <img  src="${resource(dir:'images/skin',file:'database_go.png')}" alt="Show" />
+                                    </g:link>
+                                </td>
+                            
+                            </tr>
+                        </g:each>
+                        </tbody>
+                    </table>
+                </div>
+            </g:if>
+            <div class="paginateButtons">
+                <g:paginate action="reorder" total="${inventoryItemInstanceTotal}" params="${filterParams}" />
+            </div>
+
+        </div> <!-- end body div -->
+
+        <!-- Start Search Pane -->
+        <div class="overlayPane" id="searchPane" style="display:none;">
+            <h2>Quick Search</h2>
+
+            <g:form method="post" id="searchForm" name="searchForm" >
+                <g:hiddenField name="newTextSearch" value="true" />
+
+                <table>
+                    <tbody>
+
+                        <tr class="prop">
+                            <td valign="top" class="name">
+                                <label>Inventory:</label>
+                            </td>
+                            <td valign="top" class="value">
+                                <g:link controller="inventoryItemDetailed"
+                                                action="reorder"
+                                                params="[quickSearch: 'all']">
+                                                <g:message code="default.all.text" />
+                                </g:link>
+                                <br />
+                                <g:link controller="inventoryItemDetailed"
+                                                action="reorder"
+                                                params="[quickSearch: 'inventoryBelowReorder']">
+                                                <g:message code="inventoryItem.search.text.below.reorder" />
+                                </g:link> - <g:message code="inventoryItem.search.text.below.reorder.description" />
+                                <br />
+                                <g:link controller="inventoryItemDetailed"
+                                                action="reorder"
+                                                params="[quickSearch: 'inventoryBelowReorderAll']">
+                                                <g:message code="inventoryItem.search.text.below.reorder.all" />
+                                </g:link> - <g:message code="inventoryItem.search.text.below.reorder.all.description" />
+                                <br />
+                                <g:link controller="inventoryItemDetailed"
+                                                action="reorder"
+                                                params="[quickSearch: 'recentlyUsed', daysBack: '14']">
+                                                <g:message code="inventoryItem.search.text.recently.used" />
+                                </g:link> - <g:message code="inventoryItem.search.text.recently.used.description" args="[14]"/>
+                                <br />
+                                <g:link controller="inventoryItemDetailed"
+                                                action="reorder"
+                                                params="[quickSearch: 'recentlyUsed', daysBack: '30']">
+                                                <g:message code="inventoryItem.search.text.recently.used" />
+                                </g:link> - <g:message code="inventoryItem.search.text.recently.used.description" args="[30]"/>
+                            </td>
+                        </tr>
+
+                        <tr class="prop">
+                            <td valign="top" class="name">
+                                <label>Links:</label>
+                            </td>
+                            <td valign="top" class="value">
+                                <g:link controller="inventoryItemPurchaseDetailed"
+                                                action="search">
+                                                Purchases
+                                </g:link>
+                            </td>
+                        </tr>
+
+                    </tbody>
+                </table>
+
+                <div class="buttons">
+                    <span class="button">
+                        <g:actionSubmit class="cancel" value="${g.message(code:'fp.tag.filterPane.button.cancel.text', default:'Cancel')}" onclick="return hideElement('searchPane');" />
+                    </span>
+                </div>
+            </g:form>
+        </div> <!-- end search pane -->
+
+    </body>
+</html>
