Index: trunk/grails-app/conf/Config.groovy
===================================================================
--- trunk/grails-app/conf/Config.groovy	(revision 439)
+++ trunk/grails-app/conf/Config.groovy	(revision 440)
@@ -104,4 +104,5 @@
             warn "grails.app.controller"
             info "grails.app.service.AssetCsvService"
+            info "grails.app.service.PersonCsvService"
             info "grails.app.service.InventoryCsvService"
             break
@@ -306,4 +307,12 @@
             [order:91, controller:'productionReferenceDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }]
         ]
+    ],
+    [order:170, controller:'costCodeDetailed', title:'costCode', action:'list',
+        subItems: [
+            [order:10, controller:'costCodeDetailed', title:'Cost Code List', action:'list', isVisible: { true }],
+            [order:20, controller:'costCodeDetailed', title:'Create', action:'create', isVisible: { true }],
+            [order:90, controller:'costCodeDetailed', title:'Show', action:'show', isVisible: { params.action == 'show' }],
+            [order:91, controller:'costCodeDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }]
+        ]
     ]
 ]
Index: trunk/grails-app/controllers/PersonController.groovy
===================================================================
--- trunk/grails-app/controllers/PersonController.groovy	(revision 439)
+++ trunk/grails-app/controllers/PersonController.groovy	(revision 440)
@@ -1,9 +1,11 @@
 import org.codehaus.groovy.grails.plugins.springsecurity.Secured
+import org.codehaus.groovy.grails.commons.ConfigurationHolder
 
 @Secured(['ROLE_Manager','ROLE_AppAdmin'])
 class PersonController extends BaseAppAdminController {
 
+    def filterService
+    def personCsvService
     def authenticateService
-    def filterService
 
     // the delete, save and update actions only accept POST requests
@@ -12,4 +14,39 @@
     def index = {
         redirect action: list, params: params
+    }
+
+    /**
+    * Disaply the import view.
+    */
+    def importPersons = {
+    }
+
+    /**
+    * Handle the import save.
+    */
+    def importPersonsSave = {
+        def result = personCsvService.importPersons(request)
+
+        if(!result.error) {
+            response.contentType = ConfigurationHolder.config.grails.mime.types["text"]
+            response.setHeader("Content-disposition", "attachment; filename=LoginNamesAndPasswords.txt")
+            render result.loginNamesAndPasswords
+            return
+        }
+
+        flash.errorMessage = g.message(code: result.error.code, args: result.error.args)
+        redirect(action: importPersons)
+    }
+
+    /**
+    * 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.
+    */
+    def exportPersonsTemplate = {
+        response.contentType = ConfigurationHolder.config.grails.mime.types["csv"]
+        response.setHeader("Content-disposition", "attachment; filename=personsTemplate.csv")
+        def s = personCsvService.buildPersonsTemplate()
+        render s
     }
 
Index: trunk/grails-app/i18n/messages.properties
===================================================================
--- trunk/grails-app/i18n/messages.properties	(revision 439)
+++ trunk/grails-app/i18n/messages.properties	(revision 440)
@@ -7,4 +7,10 @@
 inventory.import.success=Inventory imported.
 inventory.import.failure=Could not create inventory from supplied file, failed on line {0}, see {1}.
+
+inventoryItemPurchase.import.success=Inventory item purchases imported.
+inventoryItemPurchase.import.failure=Could not create inventory item purchases from supplied file, failed on line {0}, see {1}.
+
+person.import.success=Person list imported.
+person.import.failure=Could not create persons from supplied file, failed on line {0}, see {1}.
 
 asset.copy.subItem.create.failure=Could not complete operation, as sub item failed to save.
@@ -138,4 +144,8 @@
 inventoryMovement.still.associated=Could not complete operation as inventory movements are still associated with this item.
 
+inventoryItemPurchase.invoiceNumber.required=An invoice number must be supplied to approve payment.
+inventoryItemPurchase.delete.failure.received.exists=Could not delete, items have been received.
+inventoryItemPurchase.delete.failure.payment.approved=Could not delete, payment has been approved.
+
 assignedGroup.task.not.found=Please select a task and then ''Add Assigned Group''.
 assignedPerson.task.not.found=Please select a task and then ''Add Assigned Person''.
Index: trunk/grails-app/services/PersonCsvService.groovy
===================================================================
--- trunk/grails-app/services/PersonCsvService.groovy	(revision 440)
+++ trunk/grails-app/services/PersonCsvService.groovy	(revision 440)
@@ -0,0 +1,238 @@
+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 PersonCsvService {
+
+    boolean transactional = false
+
+    def authService
+
+    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 persons creating items as required.
+    */
+    def importPersons(request) {
+        Person.withTransaction { status ->
+            def result = [:]
+
+            def kByteMultiplier = 1000
+            def fileMaxSize = 800 * 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"])
+
+            def line = []
+            def lineNumber = 0
+            def maxNumberOfColumns = 13
+            def personParams = [:]
+            def personProperties = ["loginName", "firstName", "lastName",
+                                                    "ROLE_Manager", "ROLE_AppUser",
+                                                    "ROLE_TaskManager", "ROLE_TaskUser",
+                                                    "ROLE_InventoryManager", "ROLE_InventoryUser",
+                                                    "ROLE_AssetManager", "ROLE_AssetUser",
+                                                    "ROLE_ProductionManager", "ROLE_ProductionUser"]
+
+            def personInstance
+            def loginNamesAndPasswords = [:]
+
+            def nextLine = {
+                    line = reader.readNext()
+                    lineNumber ++
+                    log.info "Processing line: " + lineNumber
+            }
+
+            // 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: "person.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.
+                personParams = [:]
+                line.eachWithIndex { it, j ->
+                    personParams."${personProperties[j]}" = it.trim()
+                }
+
+                // Debug
+                log.debug " Supplied params: "
+                log.debug personParams
+
+                // Ignore blank lines.
+                if(personParams.loginName == '') {
+                    log.info "No login name found."
+                    nextLine()
+                    continue
+                }
+
+                // Login Name.
+                personParams.loginName = personParams.loginName.toLowerCase()
+
+                // First Name.
+                personParams.firstName = WordUtils.capitalizeFully(personParams.firstName)
+
+                // First Name.
+                personParams.lastName = WordUtils.capitalizeFully(personParams.lastName)
+
+                // Password.
+                personParams.pass = personParams.pass ?: authService.randomPassword
+
+                // Debug
+                log.debug "personParams: "
+                log.debug personParams
+
+                personInstance = Person.findByLoginName(personParams.loginName)
+
+                if(!personInstance) {
+                    log.info "Creating person with login name: " + personParams.loginName
+                    personInstance = new Person(loginName: personParams.loginName,
+                                                                                firstName: personParams.firstName,
+                                                                                lastName: personParams.lastName,
+                                                                                pass: personParams.pass,
+                                                                                password: authService.encodePassword(personParams.pass))
+
+                    // Save.
+                    if(personInstance.hasErrors() || !personInstance.save()) {
+                        log.error "Failed to create person on line: " + lineNumber
+                        log.debug personInstance.errors
+                        return fail(code: "person.import.failure", args: [lineNumber, logFileLink])
+                    }
+
+                    // Fill map with persons and passwords.
+                    loginNamesAndPasswords."${personParams.loginName}" = personParams.pass
+                }
+                else
+                    log.info "Person already exists with login name: " + personParams.loginName
+
+                // Add Authorities.
+                //personInstance.addToAuthorities(Authority.get(1))
+
+                if(lineNumber % 100 == 0)
+                    cleanUpGorm()
+
+                if(!result.error) nextLine()
+            } //while(line)
+
+            // Success.
+            log.info "End of file."
+            result.loginNamesAndPasswords = g.message(code: "person.import.success") + '\n'
+            result.loginNamesAndPasswords += "Login names and passwords: " + '\n'
+            result.loginNamesAndPasswords += '\n'
+            loginNamesAndPasswords.each() { result.loginNamesAndPasswords += (it.key + ' : ' + it.value + '\n') }
+            reader.close()
+            return result
+
+         } //end withTransaction
+    } // end importPersons()
+
+    /**
+    * Build a persons template csv file.
+    * This template can then be populated for import.
+    * @returns The template as a String in csv format.
+    */
+    def buildPersonsTemplate() {
+
+        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: Role columns must be 'true' or 'false'.")
+        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()
+    }
+
+    private getTemplateHeaderLine1() {
+            ["Login Name*", "First Name*", "Last Name*",
+            "Role: Business Manager*", "Role: Application User*",
+            "Role: Task Manager*", "Role: Task User*",
+            "Role: Inventory Manager*", "Role: Inventory User*",
+            "Role: Asset Manager*", "Role: Asset User*",
+            "Role: Production Manager*", "Role: Production User*"]
+    }
+
+    /**
+    * 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/person/importPersons.gsp
===================================================================
--- trunk/grails-app/views/person/importPersons.gsp	(revision 440)
+++ trunk/grails-app/views/person/importPersons.gsp	(revision 440)
@@ -0,0 +1,35 @@
+<html>
+    <head>
+        <meta name="layout" content="main" />
+        <title>Import Persons</title>
+        <nav:resources override="true"/>
+        <g:render template="/shared/pictureHead" />
+    </head>
+    <body>
+        <div class="nav">
+            <h1>Import Persons</h1>
+        </div>
+        <div class="body">
+            <g:render template="/shared/messages" />
+            <g:uploadForm action="importPersonsSave" 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/person/list.gsp
===================================================================
--- trunk/grails-app/views/person/list.gsp	(revision 439)
+++ trunk/grails-app/views/person/list.gsp	(revision 440)
@@ -29,4 +29,41 @@
             Results:${personTotal}
         </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 id="options" style="display:none;">
+            <g:form method="post" >
+                <g:hiddenField name="params" value="${filterParams}" />
+                <div class="dialog">
+                    <table>
+                        <tbody>
+
+                            <tr class="prop">
+                                <td valign="top" class="name">
+                                    <label for="max">Persons:</label>
+                                </td>
+                                <td valign="top" class="value">
+                                    <g:link action="exportPersonsTemplate">
+                                        Template
+                                    </g:link>
+                                    /
+                                    <g:link action="importPersons">
+                                        Import
+                                    </g:link>
+                                </td>
+                            </tr>
+
+                        </tbody>
+                    </table>
+                </div>
+            </g:form>
+        </div>
+
+        <br />
 
         <div class="list">
