Index: /trunk/grails-app/controllers/InventoryItemDetailedController.groovy
===================================================================
--- /trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 422)
+++ /trunk/grails-app/controllers/InventoryItemDetailedController.groovy	(revision 423)
@@ -7,4 +7,5 @@
     def filterService
     def exportService
+    def inventoryCsvService
     def inventoryItemService
     def inventoryMovementService
@@ -30,4 +31,63 @@
         }
         forward(action: 'search', params: params)
+    }
+
+    /**
+    * Disaply the import view.
+    */
+    def importInventory = {
+    }
+
+    /**
+    * Handle the import save.
+    */
+    def importInventorySave = {
+        def result = inventoryCsvService.importInventory(request)
+
+        if(!result.error) {
+            flash.message = g.message(code: "inventory.import.success")
+            redirect(action:search)
+            return
+        }
+
+        flash.errorMessage = g.message(code: result.error.code, args: result.error.args)
+        redirect(action: importInventory)
+    }
+
+    /**
+    * Export a csv template.
+    * NOTE: IE has a 'validating' bug in dev mode that causes the export to take a long time!
+    * This does not appear to be a problem once deployed to Tomcat.
+    */
+    @Secured(['ROLE_AppAdmin', 'ROLE_Manager', 'ROLE_InventoryManager', 'ROLE_InventoryUser'])
+    def exportInventoryTemplate = {
+        response.contentType = ConfigurationHolder.config.grails.mime.types["csv"]
+        response.setHeader("Content-disposition", "attachment; filename=InventoryTemplate.csv")
+        def s = inventoryCsvService.buildInventoryTemplate()
+        render s
+    }
+
+    /**
+    * Export a csv test file.
+    */
+    def exportInventoryExample = {
+        response.contentType = ConfigurationHolder.config.grails.mime.types["csv"]
+        response.setHeader("Content-disposition", "attachment; filename=InventoryExample.csv")
+        def s = inventoryCsvService.buildInventoryExample()
+        render s
+    }
+
+    /**
+    * Export the entire inventory as a csv file.
+    */
+    @Secured(['ROLE_AppAdmin', 'ROLE_Manager', 'ROLE_InventoryManager', 'ROLE_InventoryUser'])
+    def exportInventory = {
+
+        def inventoryItemList = InventoryItem.list()
+
+        response.contentType = ConfigurationHolder.config.grails.mime.types["csv"]
+        response.setHeader("Content-disposition", "attachment; filename=Inventory.csv")
+        def s = inventoryCsvService.buildInventory(inventoryItemList)
+        render s
     }
 
Index: /trunk/grails-app/services/InventoryCsvService.groovy
===================================================================
--- /trunk/grails-app/services/InventoryCsvService.groovy	(revision 423)
+++ /trunk/grails-app/services/InventoryCsvService.groovy	(revision 423)
@@ -0,0 +1,556 @@
+import grails.util.GrailsUtil
+import au.com.bytecode.opencsv.CSVWriter
+import au.com.bytecode.opencsv.CSVReader
+import org.apache.commons.lang.WordUtils
+
+/**
+ * Provides some csv import/export methods.
+ * Requires the opencsv jar to be available which is included in the grails-export plugin.
+ */
+class InventoryCsvService {
+
+    boolean transactional = false
+
+    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()
+
+    def sessionFactory
+    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP
+
+    /**
+    * Import inventory creating items as required.
+    * @param request The http request to run getFile against.
+    * Get file should return a csv format file containing the inventory as per template.
+    */
+    def importInventory(request) {
+        InventoryItem.withTransaction { status ->
+            def result = [:]
+
+            def kByteMultiplier = 1000
+            def fileMaxSize = 500 * kByteMultiplier
+            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}
+
+            def multiPartFile = request.getFile('file')
+
+            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
+            CSVReader reader = new CSVReader(sr)
+
+            def fail = { Map m ->
+                status.setRollbackOnly()
+                reader.close()
+                result.error = [ code: m.code, args: m.args ]
+                return result
+            }
+
+            if(!multiPartFile || multiPartFile.isEmpty())
+                return fail(code: "default.file.not.supplied")
+
+            if (multiPartFile.getSize() > fileMaxSize)
+                return fail(code: "default.file.over.max.size", args: [fileMaxSize/kByteMultiplier, "kB"])
+
+            //TODO: delete
+            def columnValue = ''
+            def columnIndex = 0
+            def numberOfColumns = 0
+
+            def line = []
+            def lineNumber = 0
+            def maxNumberOfColumns = 23
+            def inventoryParams = [:]
+            def inventoryProperties = ["name", "description", "comment", "unitsInStock", "reorderPoint", "recommendedReorderPoint",
+                                                        "unitOfMeasure", "estimatedUnitPriceAmount", "estimatedUnitPriceCurrency",
+                                                        "enableReorder", "inventoryLocation", "inventoryStore", "site",
+                                                        "inventoryGroup", "inventoryType", "averageDeliveryTime", "averageDeliveryPeriod",
+                                                        "suppliersPartNumber", "suppliers",
+                                                        "manufacturersPartNumber", "manufacturers", "alternateItems", "spareFor"]
+
+            def siteInstance
+            def supplierInstance
+            def supplierTypeInstance
+            def supplierTypeUnknown = SupplierType.get(1)
+            def spareForInstance
+            def alternateItemInstance
+            def manufacturerInstance
+            def manufacturerTypeInstance
+            def manufacturerTypeUnknown = ManufacturerType.get(1)
+            def inventoryTypeInstance
+            def unitOfMeasureInstance
+            def inventoryGroupInstance
+            def inventoryItemInstance
+            def inventoryStoreInstance
+            def inventoryLocationInstance
+            def averageDeliveryPeriodInstance
+
+            def tempSuppliers = []
+            def tempSupplierItemAndType = []
+            def tempManufacturers = []
+            def tempManufacturerItemAndType = []
+
+            def tempSpareFor = []
+            def tempAlternateItems = []
+
+            def column = ''
+
+            def nextLine = {
+                    line = reader.readNext()
+                    lineNumber ++
+                    log.info "Processing line: " + lineNumber
+            }
+
+            def nextColumn = {
+
+                if( columnIndex < numberOfColumns ) {
+                        column = line[columnIndex].trim()
+                }
+                else {
+                    log.info "No more columns on line: " + lineNumber
+                    return false
+                }
+
+                columnIndex++
+                // Success.
+                return column
+            }
+
+            def parseInputList = {
+                if(it.trim() == '') return []
+                return it.split(";").collect{it.trim()}
+            }
+
+            def parseItemAndType = {
+                return it.split("@").collect{it.trim()}
+            }
+
+            // Get first line.
+            nextLine()
+
+            // Check for header line 1.
+            if(line != templateHeaderLine1) {
+                log.error "Failed to find header line 1. "
+                log.error "Required: " + templateHeaderLine1.toString()
+                log.error "Supplied: " + line.toString()
+                return fail(code: "default.file.no.header")
+            }
+
+            log.info "Header line found."
+
+            // Prepare the first body line.
+            nextLine()
+
+            // Primary loop.
+            while(line) {
+
+                if(line.size() > maxNumberOfColumns) {
+                    log.error "Too many columns on line: " + lineNumber
+                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                }
+
+                // Ignore comment lines.
+                if(line.toString().toLowerCase().contains("comment")) {
+                    log.info "Comment line found."
+                    nextLine()
+                    continue
+                }
+
+                // Ignore example lines.
+                if(line.toString().toLowerCase().contains("example")) {
+                    log.info "Example line found."
+                    nextLine()
+                    continue
+                }
+
+                // Parse the line into the params map.
+                /** TODO: capitalize and capitalizeFully.*/
+                inventoryParams = [:]
+                line.eachWithIndex { it, j ->
+                    inventoryParams."${inventoryProperties[j]}" = it.trim()
+                }
+
+                // Debug
+                log.debug " Supplied params: "
+                log.debug inventoryParams
+
+                // Ignore blank lines.
+                if(inventoryParams.name == '') {
+                    log.info "No name found."
+                    nextLine()
+                    continue
+                }
+
+                /** Prepare the params and create supporting items as required. */
+
+                // Site
+                siteInstance = Site.findByName(inventoryParams.site)
+                if(!siteInstance) {
+                    siteInstance = new Site(name: inventoryParams.site)
+                    if(!siteInstance.save()) {
+                        log.error "Failed to create site on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // InventoryStore
+                inventoryStoreInstance = InventoryStore.findByName(inventoryParams.inventoryStore)
+                if(!inventoryStoreInstance) {
+                    inventoryStoreInstance = new InventoryStore(name: inventoryParams.inventoryStore,
+                                                                                                site: siteInstance)
+                    if(!inventoryStoreInstance.save()) {
+                        log.error "Failed to create inventory store on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // InventoryLocation
+                inventoryLocationInstance = InventoryLocation.findByName(inventoryParams.inventoryLocation)
+                if(!inventoryLocationInstance) {
+                    inventoryLocationInstance = new InventoryLocation(name: inventoryParams.inventoryLocation,
+                                                                                                        inventoryStore: inventoryStoreInstance)
+                    if(!inventoryLocationInstance.save()) {
+                        log.error "Failed to create inventory location on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // InventoryGroup
+                inventoryGroupInstance = InventoryGroup.findByName(inventoryParams.inventoryGroup)
+                if(!inventoryGroupInstance) {
+                    inventoryGroupInstance = new InventoryGroup(name: inventoryParams.inventoryGroup)
+                    if(!inventoryGroupInstance.save()) {
+                        log.error "Failed to create inventory group on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // InventoryType
+                inventoryTypeInstance = InventoryType.findByName(inventoryParams.inventoryType)
+                if(!inventoryTypeInstance) {
+                    inventoryTypeInstance = new InventoryType(name: inventoryParams.inventoryType)
+                    if(!inventoryTypeInstance.save()) {
+                        log.error "Failed to create inventory type on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // UnitOfMeasure.
+                unitOfMeasureInstance = UnitOfMeasure.findByName(inventoryParams.unitOfMeasure)
+                if(!unitOfMeasureInstance) {
+                    unitOfMeasureInstance = new UnitOfMeasure(name: inventoryParams.unitOfMeasure)
+                    if(!unitOfMeasureInstance.save()) {
+                        log.error "Failed to create unit of measure on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // AverageDeliveryPeriod.
+                if(inventoryParams.averageDeliveryPeriod) {
+                    averageDeliveryPeriodInstance = Period.findByPeriod(inventoryParams.averageDeliveryPeriod)
+                    if(!averageDeliveryPeriodInstance) {
+                        log.error "Failed, not a valid delivery period on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+                }
+
+                // Manufacturers.
+                tempManufacturers = parseInputList(inventoryParams.manufacturers)
+                inventoryParams.manufacturers = []
+
+                for(tempManufacturer in tempManufacturers) {
+                    tempManufacturerItemAndType = parseItemAndType(tempManufacturer)
+
+                    manufacturerInstance = Manufacturer.findByName(tempManufacturerItemAndType[0])
+                    if(!manufacturerInstance) {
+
+                        // ManufacturerType.
+                        if(tempManufacturerItemAndType.size == 2)
+                            manufacturerTypeInstance = ManufacturerType.findByName(tempManufacturerItemAndType[1])
+                        else
+                            manufacturerTypeInstance = manufacturerTypeUnknown
+                        if(!manufacturerTypeInstance) {
+                            log.error "Failed to find manufacturer type on line: " + lineNumber
+                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                        }
+
+                        manufacturerInstance = new Manufacturer(name: tempManufacturerItemAndType[0],
+                                                                                                manufacturerType: manufacturerTypeInstance)
+                        if(!manufacturerInstance.save()) {
+                            log.error "Failed to create manufacturers on line: " + lineNumber
+                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                        }
+                    }
+
+                    inventoryParams.manufacturers.add(manufacturerInstance)
+                }
+
+                // Suppliers.
+                tempSuppliers = parseInputList(inventoryParams.suppliers)
+                inventoryParams.suppliers = []
+
+                for(tempSupplier in tempSuppliers) {
+                    tempSupplierItemAndType = parseItemAndType(tempSupplier)
+
+                    supplierInstance = Supplier.findByName(tempSupplierItemAndType[0])
+                    if(!supplierInstance) {
+
+                        // SupplierType.
+                        if(tempSupplierItemAndType.size == 2)
+                            supplierTypeInstance = SupplierType.findByName(tempSupplierItemAndType[1])
+                        else
+                            supplierTypeInstance = supplierTypeUnknown
+                        if(!supplierTypeInstance) {
+                            log.error "Failed to find supplier type on line: " + lineNumber
+                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                        }
+
+                        supplierInstance = new Supplier(name: tempSupplierItemAndType[0],
+                                                                            supplierType: supplierTypeInstance)
+                        if(!supplierInstance.save()) {
+                            log.error "Failed to create suppliers on line: " + lineNumber
+                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                        }
+                    }
+
+                    inventoryParams.suppliers.add(supplierInstance)
+                }
+
+                // AlternateItems.
+                tempAlternateItems = parseInputList(inventoryParams.alternateItems)
+                inventoryParams.alternateItems = []
+
+                for(tempAlternateItem in tempAlternateItems) {
+
+                    alternateItemInstance = InventoryItem.findByName(tempAlternateItem)
+                    if(!alternateItemInstance) {
+                        alternateItemInstance = new InventoryItem(name: tempAlternateItem,
+                                                                                                description: "Generated from alternateItems during import, details may not be correct.",
+                                                                                                reorderPoint: 0,
+                                                                                                inventoryGroup: inventoryGroupInstance,
+                                                                                                inventoryType: inventoryTypeInstance,
+                                                                                                unitOfMeasure: unitOfMeasureInstance,
+                                                                                                inventoryLocation: inventoryLocationInstance)
+                        if(!alternateItemInstance.save()) {
+                            log.error "Failed to create alternateItems on line: " + lineNumber
+                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                        }
+                    }
+
+                    inventoryParams.alternateItems.add(alternateItemInstance)
+                }
+
+                // spareFor.
+                tempSpareFor = parseInputList(inventoryParams.spareFor)
+                inventoryParams.spareFor = []
+
+                for(asset in tempSpareFor) {
+
+                    println ''
+                    println 'asset: '+asset
+                    println ''
+
+                    spareForInstance = Asset.findByName(asset)
+                    if(!spareForInstance) {
+                        log.error "Failed to find 'Spare For' asset on line: " + lineNumber
+                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                    }
+
+                    inventoryParams.spareFor.add(spareForInstance)
+                }
+
+                // Assign the retrieved or created instances to params.
+                inventoryParams.inventoryLocation = inventoryLocationInstance
+                inventoryParams.inventoryGroup = inventoryGroupInstance
+                inventoryParams.inventoryType = inventoryTypeInstance
+                inventoryParams.unitOfMeasure = unitOfMeasureInstance
+                inventoryParams.averageDeliveryPeriod = averageDeliveryPeriodInstance
+
+                // Debug
+                log.debug "InventoryParams: "
+                log.debug inventoryParams
+
+                // Create new or update.
+                inventoryItemInstance = InventoryItem.findByName(inventoryParams.name)
+                if(inventoryItemInstance) {
+                    log.info "Updating existing item: " + inventoryItemInstance
+                    inventoryItemInstance.properties = inventoryParams
+                }
+                else {
+                    log.info "Creating new item: " + inventoryParams.name
+                    inventoryItemInstance = new InventoryItem(inventoryParams)
+                }
+
+                // Save inventoryItem.
+                if(inventoryItemInstance.hasErrors() || !inventoryItemInstance.save()) {
+                    log.error "Failed to create item on line: " + column + "(" + lineNumber + ")"
+                    log.debug inventoryItemInstance.errors
+                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
+                }
+
+                if(lineNumber % 100 == 0)
+                    cleanUpGorm()
+
+                if(!result.error) nextLine()
+            } //while(line)
+
+            // Success.
+            log.info "End of file."
+            reader.close()
+            return result
+
+        } //end withTransaction
+    } // end importInventory()
+
+    /**
+    * Build an inventory template csv file.
+    * This template can then be populated for import.
+    * @returns The template as a String in csv format.
+    */
+    def buildInventoryTemplate() {
+
+        StringWriter sw = new StringWriter()
+        CSVWriter writer = new CSVWriter(sw)
+
+        writeTemplateLines(writer)
+
+        writer.close()
+        return sw.toString()
+    }
+
+    private writeTemplateLines(writer) {
+        writer.writeNext(templateHeaderLine1 as String[])
+        writer.writeNext()
+        writer.writeNext("Comment: The header line is required.")
+        writer.writeNext("Comment: Required columns are marked with a (*) in the header line.")
+        writer.writeNext("Comment: Lists of items in a column must be separated by a semicolon (;), not a comma.")
+        writer.writeNext("Comment: The at symbol (@) is reserved for indicating supplier and manufacturer types.")
+        writer.writeNext("Comment: Identical and existing names will be considered as the same item.")
+        writer.writeNext("Comment: Lines containing 'comment' will be ignored.")
+        writer.writeNext("Comment: Lines containing 'example' will be ignored.")
+        writer.writeNext("Comment: This file must be saved as a CSV file before import.")
+        writer.writeNext()
+    }
+
+    /**
+    * Build an inventory example/test file.
+    * This test file can be imported to test the import and export methods.
+    * @returns The test file as a String in csv format.
+    */
+    def buildInventoryExample() {
+
+        StringWriter sw = new StringWriter()
+        CSVWriter writer = new CSVWriter(sw)
+
+        writeTemplateLines(writer)
+
+        // Requires creation of some of the base/group/type data.
+        writer.writeNext(["Split19", "19mm split pin", "Very usefull item.",
+                                        "1024", "0", "1",
+                                        "each", "5", "NZD",
+                                        "false", "BR4",
+                                        "Store #99", "Inventory Depot",
+                                        "Mechanical Stock",
+                                        "Consumable",
+                                        "7", "Week(s)",
+                                        "123", "Multi Distributors1@OEM; Multi Distributors2@Local",
+                                        "321", "Mega Manufacturer1@OEM;Mega Manufacturer2@Alternate",
+                                        "2204E-2RS", ""
+                                        ] as String[])
+
+        // Using existing base data.
+        writer.writeNext(["2204E-2RS", "Double Row Self Align Ball Bearing 2204E-2RS - Sealed - 20/47x18", "",
+                                        "4", "1", "9",
+                                        "each", "16.35", "USD",
+                                        "TRUE", "BR4",
+                                        "Store #99", "Inventory Depot",
+                                        "Mechanical Stock",
+                                        "Consumable",
+                                        "2", "Month(s)",
+                                        "456KL", "Multi Distributors1; Multi Distributors2",
+                                        "654OP", "Mega Manufacturer1;Mega Manufacturer2",
+                                        "", ""
+                                        ] as String[])
+
+        writer.close()
+        return sw.toString()
+    }
+
+    /**
+    * Build complete inventory for export.
+    * @param inventoryItemList The list of inventory items to build..
+    * @returns The inventory as a String in csv format.
+    */
+    def buildInventory(List inventoryItemList) {
+
+        def sw = new StringWriter()
+        def writer = new CSVWriter(sw)
+
+        writeTemplateLines(writer)
+
+        //Rows
+        def row
+
+        inventoryItemList.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { inventoryItem ->
+            row = []
+            row.add(inventoryItem.name)
+            row.add(inventoryItem.description)
+            row.add(inventoryItem.comment)
+            row.add(inventoryItem.unitsInStock)
+            row.add(inventoryItem.reorderPoint)
+            row.add(inventoryItem.recommendedReorderPoint)
+            row.add(inventoryItem.unitOfMeasure)
+            row.add(inventoryItem.estimatedUnitPriceAmount)
+            row.add(inventoryItem.estimatedUnitPriceCurrency)
+            row.add(inventoryItem.enableReorder)
+            row.add(inventoryItem.inventoryLocation)
+            row.add(inventoryItem.inventoryLocation.inventoryStore)
+            row.add(inventoryItem.inventoryLocation.inventoryStore.site)
+            row.add(inventoryItem.inventoryGroup)
+            row.add(inventoryItem.inventoryType)
+            row.add(inventoryItem.averageDeliveryTime)
+            row.add(inventoryItem.averageDeliveryPeriod)
+            row.add(inventoryItem.suppliersPartNumber)
+
+            row.add( inventoryItem.suppliers.sort { p1, p2 ->
+                p1.name.compareToIgnoreCase(p2.name)
+            }.collect { it.name + "@" + it.supplierType }.join(';') )
+
+            row.add(inventoryItem.manufacturersPartNumber)
+
+            row.add(inventoryItem.manufacturers.sort { p1, p2 ->
+                p1.name.compareToIgnoreCase(p2.name)
+            }.collect { it.name + "@" + it.manufacturerType }.join(';'))
+
+            row.add(inventoryItem.alternateItems.sort { p1, p2 ->
+                p1.name.compareToIgnoreCase(p2.name)
+            }.collect { it.name }.join(';') )
+
+            row.add(inventoryItem.spareFor.sort { p1, p2 ->
+                p1.name.compareToIgnoreCase(p2.name)
+            }.collect { it.name }.join(';'))
+
+            writer.writeNext(row as String[])
+        }
+
+        writer.close()
+        return sw.toString()
+    } // end buildInventory
+
+    private getTemplateHeaderLine1() {
+            ["Name*", "Description", "Comment", "Units In Stock", "Reorder Point*", "Recommended Reorder Point", "Unit Of Measure*",
+            "Estimated Unit Price", "Currency", "Enable Reorder", "Location*", "Store*", "Site*", "Group*", "Type*",
+            "averageDeliveryTime", "averageDeliveryPeriod", "Supplier's Part Number", "Supplier",
+            "Manufacturer's Part Number", "Manufacturer", "Alternate Item", "Spare For"]
+    }
+
+    /**
+    * This cleans up the hibernate session and a grails map.
+    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
+    * The hibernate session flush is normal for hibernate.
+    * The map is apparently used by grails for domain object validation errors.
+    * A starting point for clean up is every 100 objects.
+    */
+    def cleanUpGorm() {
+        def session = sessionFactory.currentSession
+        session.flush()
+        session.clear()
+        propertyInstanceMap.get().clear()
+    }
+
+} // end class
Index: /trunk/grails-app/views/inventoryItemDetailed/importInventory.gsp
===================================================================
--- /trunk/grails-app/views/inventoryItemDetailed/importInventory.gsp	(revision 423)
+++ /trunk/grails-app/views/inventoryItemDetailed/importInventory.gsp	(revision 423)
@@ -0,0 +1,35 @@
+<html>
+    <head>
+        <meta name="layout" content="main" />
+        <title>Import Inventory</title>
+        <nav:resources override="true"/>
+        <g:render template="/shared/pictureHead" />
+    </head>
+    <body>
+        <div class="nav">
+            <h1>Import Inventory</h1>
+        </div>
+        <div class="body">
+            <g:render template="/shared/messages" />
+            <g:uploadForm action="importInventorySave" onsubmit="return Lightbox.loading();">
+                <div class="dialog">
+                    <table>
+                        <tbody>
+                            <tr class="prop">
+                                <td valign="top" class="name">
+                                    <label for="file">File:</label>
+                                </td>
+                                <td valign="top" class="value">
+                                    <input type="file" id="file" name="file" size="40"/>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+                <div class="buttons">
+                    <span class="button"><input class="save" type="submit" value="Create" /></span>
+                </div>
+            </g:uploadForm>
+        </div>
+    </body>
+</html>
Index: /trunk/grails-app/views/inventoryItemDetailed/search.gsp
===================================================================
--- /trunk/grails-app/views/inventoryItemDetailed/search.gsp	(revision 422)
+++ /trunk/grails-app/views/inventoryItemDetailed/search.gsp	(revision 423)
@@ -55,4 +55,27 @@
                                             <g:actionSubmit action="setSearchParamsMax" class="go" value="Update" />
                                         </span>
+                                    </td>
+                                </tr>
+
+                                <tr class="prop">
+                                    <td valign="top" class="name">
+                                        <label for="max">Inventory:</label>
+                                    </td>
+                                    <td valign="top" class="value">
+                                        <g:link action="exportInventory">
+                                            Export
+                                        </g:link>
+                                        /
+                                        <g:link action="exportInventoryTemplate">
+                                            Template
+                                        </g:link>
+                                        /
+                                        <g:link action="exportInventoryExample">
+                                            Example
+                                        </g:link>
+                                        /
+                                        <g:link action="importInventory">
+                                            Import
+                                        </g:link>
                                     </td>
                                 </tr>
