source: trunk/grails-app/services/InventoryCsvService.groovy @ 423

Last change on this file since 423 was 423, checked in by gav, 10 years ago

Add Inventory import/export functionality.

File size: 24.5 KB
Line 
1import grails.util.GrailsUtil
2import au.com.bytecode.opencsv.CSVWriter
3import au.com.bytecode.opencsv.CSVReader
4import org.apache.commons.lang.WordUtils
5
6/**
7 * Provides some csv import/export methods.
8 * Requires the opencsv jar to be available which is included in the grails-export plugin.
9 */
10class InventoryCsvService {
11
12    boolean transactional = false
13
14    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()
15
16    def sessionFactory
17    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP
18
19    /**
20    * Import inventory creating items as required.
21    * @param request The http request to run getFile against.
22    * Get file should return a csv format file containing the inventory as per template.
23    */
24    def importInventory(request) {
25        InventoryItem.withTransaction { status ->
26            def result = [:]
27
28            def kByteMultiplier = 1000
29            def fileMaxSize = 500 * kByteMultiplier
30            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}
31
32            def multiPartFile = request.getFile('file')
33
34            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
35            CSVReader reader = new CSVReader(sr)
36
37            def fail = { Map m ->
38                status.setRollbackOnly()
39                reader.close()
40                result.error = [ code: m.code, args: m.args ]
41                return result
42            }
43
44            if(!multiPartFile || multiPartFile.isEmpty())
45                return fail(code: "default.file.not.supplied")
46
47            if (multiPartFile.getSize() > fileMaxSize)
48                return fail(code: "default.file.over.max.size", args: [fileMaxSize/kByteMultiplier, "kB"])
49
50            //TODO: delete
51            def columnValue = ''
52            def columnIndex = 0
53            def numberOfColumns = 0
54
55            def line = []
56            def lineNumber = 0
57            def maxNumberOfColumns = 23
58            def inventoryParams = [:]
59            def inventoryProperties = ["name", "description", "comment", "unitsInStock", "reorderPoint", "recommendedReorderPoint",
60                                                        "unitOfMeasure", "estimatedUnitPriceAmount", "estimatedUnitPriceCurrency",
61                                                        "enableReorder", "inventoryLocation", "inventoryStore", "site",
62                                                        "inventoryGroup", "inventoryType", "averageDeliveryTime", "averageDeliveryPeriod",
63                                                        "suppliersPartNumber", "suppliers",
64                                                        "manufacturersPartNumber", "manufacturers", "alternateItems", "spareFor"]
65
66            def siteInstance
67            def supplierInstance
68            def supplierTypeInstance
69            def supplierTypeUnknown = SupplierType.get(1)
70            def spareForInstance
71            def alternateItemInstance
72            def manufacturerInstance
73            def manufacturerTypeInstance
74            def manufacturerTypeUnknown = ManufacturerType.get(1)
75            def inventoryTypeInstance
76            def unitOfMeasureInstance
77            def inventoryGroupInstance
78            def inventoryItemInstance
79            def inventoryStoreInstance
80            def inventoryLocationInstance
81            def averageDeliveryPeriodInstance
82
83            def tempSuppliers = []
84            def tempSupplierItemAndType = []
85            def tempManufacturers = []
86            def tempManufacturerItemAndType = []
87
88            def tempSpareFor = []
89            def tempAlternateItems = []
90
91            def column = ''
92
93            def nextLine = {
94                    line = reader.readNext()
95                    lineNumber ++
96                    log.info "Processing line: " + lineNumber
97            }
98
99            def nextColumn = {
100
101                if( columnIndex < numberOfColumns ) {
102                        column = line[columnIndex].trim()
103                }
104                else {
105                    log.info "No more columns on line: " + lineNumber
106                    return false
107                }
108
109                columnIndex++
110                // Success.
111                return column
112            }
113
114            def parseInputList = {
115                if(it.trim() == '') return []
116                return it.split(";").collect{it.trim()}
117            }
118
119            def parseItemAndType = {
120                return it.split("@").collect{it.trim()}
121            }
122
123            // Get first line.
124            nextLine()
125
126            // Check for header line 1.
127            if(line != templateHeaderLine1) {
128                log.error "Failed to find header line 1. "
129                log.error "Required: " + templateHeaderLine1.toString()
130                log.error "Supplied: " + line.toString()
131                return fail(code: "default.file.no.header")
132            }
133
134            log.info "Header line found."
135
136            // Prepare the first body line.
137            nextLine()
138
139            // Primary loop.
140            while(line) {
141
142                if(line.size() > maxNumberOfColumns) {
143                    log.error "Too many columns on line: " + lineNumber
144                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
145                }
146
147                // Ignore comment lines.
148                if(line.toString().toLowerCase().contains("comment")) {
149                    log.info "Comment line found."
150                    nextLine()
151                    continue
152                }
153
154                // Ignore example lines.
155                if(line.toString().toLowerCase().contains("example")) {
156                    log.info "Example line found."
157                    nextLine()
158                    continue
159                }
160
161                // Parse the line into the params map.
162                /** TODO: capitalize and capitalizeFully.*/
163                inventoryParams = [:]
164                line.eachWithIndex { it, j ->
165                    inventoryParams."${inventoryProperties[j]}" = it.trim()
166                }
167
168                // Debug
169                log.debug " Supplied params: "
170                log.debug inventoryParams
171
172                // Ignore blank lines.
173                if(inventoryParams.name == '') {
174                    log.info "No name found."
175                    nextLine()
176                    continue
177                }
178
179                /** Prepare the params and create supporting items as required. */
180
181                // Site
182                siteInstance = Site.findByName(inventoryParams.site)
183                if(!siteInstance) {
184                    siteInstance = new Site(name: inventoryParams.site)
185                    if(!siteInstance.save()) {
186                        log.error "Failed to create site on line: " + lineNumber
187                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
188                    }
189                }
190
191                // InventoryStore
192                inventoryStoreInstance = InventoryStore.findByName(inventoryParams.inventoryStore)
193                if(!inventoryStoreInstance) {
194                    inventoryStoreInstance = new InventoryStore(name: inventoryParams.inventoryStore,
195                                                                                                site: siteInstance)
196                    if(!inventoryStoreInstance.save()) {
197                        log.error "Failed to create inventory store on line: " + lineNumber
198                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
199                    }
200                }
201
202                // InventoryLocation
203                inventoryLocationInstance = InventoryLocation.findByName(inventoryParams.inventoryLocation)
204                if(!inventoryLocationInstance) {
205                    inventoryLocationInstance = new InventoryLocation(name: inventoryParams.inventoryLocation,
206                                                                                                        inventoryStore: inventoryStoreInstance)
207                    if(!inventoryLocationInstance.save()) {
208                        log.error "Failed to create inventory location on line: " + lineNumber
209                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
210                    }
211                }
212
213                // InventoryGroup
214                inventoryGroupInstance = InventoryGroup.findByName(inventoryParams.inventoryGroup)
215                if(!inventoryGroupInstance) {
216                    inventoryGroupInstance = new InventoryGroup(name: inventoryParams.inventoryGroup)
217                    if(!inventoryGroupInstance.save()) {
218                        log.error "Failed to create inventory group on line: " + lineNumber
219                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
220                    }
221                }
222
223                // InventoryType
224                inventoryTypeInstance = InventoryType.findByName(inventoryParams.inventoryType)
225                if(!inventoryTypeInstance) {
226                    inventoryTypeInstance = new InventoryType(name: inventoryParams.inventoryType)
227                    if(!inventoryTypeInstance.save()) {
228                        log.error "Failed to create inventory type on line: " + lineNumber
229                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
230                    }
231                }
232
233                // UnitOfMeasure.
234                unitOfMeasureInstance = UnitOfMeasure.findByName(inventoryParams.unitOfMeasure)
235                if(!unitOfMeasureInstance) {
236                    unitOfMeasureInstance = new UnitOfMeasure(name: inventoryParams.unitOfMeasure)
237                    if(!unitOfMeasureInstance.save()) {
238                        log.error "Failed to create unit of measure on line: " + lineNumber
239                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
240                    }
241                }
242
243                // AverageDeliveryPeriod.
244                if(inventoryParams.averageDeliveryPeriod) {
245                    averageDeliveryPeriodInstance = Period.findByPeriod(inventoryParams.averageDeliveryPeriod)
246                    if(!averageDeliveryPeriodInstance) {
247                        log.error "Failed, not a valid delivery period on line: " + lineNumber
248                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
249                    }
250                }
251
252                // Manufacturers.
253                tempManufacturers = parseInputList(inventoryParams.manufacturers)
254                inventoryParams.manufacturers = []
255
256                for(tempManufacturer in tempManufacturers) {
257                    tempManufacturerItemAndType = parseItemAndType(tempManufacturer)
258
259                    manufacturerInstance = Manufacturer.findByName(tempManufacturerItemAndType[0])
260                    if(!manufacturerInstance) {
261
262                        // ManufacturerType.
263                        if(tempManufacturerItemAndType.size == 2)
264                            manufacturerTypeInstance = ManufacturerType.findByName(tempManufacturerItemAndType[1])
265                        else
266                            manufacturerTypeInstance = manufacturerTypeUnknown
267                        if(!manufacturerTypeInstance) {
268                            log.error "Failed to find manufacturer type on line: " + lineNumber
269                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
270                        }
271
272                        manufacturerInstance = new Manufacturer(name: tempManufacturerItemAndType[0],
273                                                                                                manufacturerType: manufacturerTypeInstance)
274                        if(!manufacturerInstance.save()) {
275                            log.error "Failed to create manufacturers on line: " + lineNumber
276                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
277                        }
278                    }
279
280                    inventoryParams.manufacturers.add(manufacturerInstance)
281                }
282
283                // Suppliers.
284                tempSuppliers = parseInputList(inventoryParams.suppliers)
285                inventoryParams.suppliers = []
286
287                for(tempSupplier in tempSuppliers) {
288                    tempSupplierItemAndType = parseItemAndType(tempSupplier)
289
290                    supplierInstance = Supplier.findByName(tempSupplierItemAndType[0])
291                    if(!supplierInstance) {
292
293                        // SupplierType.
294                        if(tempSupplierItemAndType.size == 2)
295                            supplierTypeInstance = SupplierType.findByName(tempSupplierItemAndType[1])
296                        else
297                            supplierTypeInstance = supplierTypeUnknown
298                        if(!supplierTypeInstance) {
299                            log.error "Failed to find supplier type on line: " + lineNumber
300                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
301                        }
302
303                        supplierInstance = new Supplier(name: tempSupplierItemAndType[0],
304                                                                            supplierType: supplierTypeInstance)
305                        if(!supplierInstance.save()) {
306                            log.error "Failed to create suppliers on line: " + lineNumber
307                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
308                        }
309                    }
310
311                    inventoryParams.suppliers.add(supplierInstance)
312                }
313
314                // AlternateItems.
315                tempAlternateItems = parseInputList(inventoryParams.alternateItems)
316                inventoryParams.alternateItems = []
317
318                for(tempAlternateItem in tempAlternateItems) {
319
320                    alternateItemInstance = InventoryItem.findByName(tempAlternateItem)
321                    if(!alternateItemInstance) {
322                        alternateItemInstance = new InventoryItem(name: tempAlternateItem,
323                                                                                                description: "Generated from alternateItems during import, details may not be correct.",
324                                                                                                reorderPoint: 0,
325                                                                                                inventoryGroup: inventoryGroupInstance,
326                                                                                                inventoryType: inventoryTypeInstance,
327                                                                                                unitOfMeasure: unitOfMeasureInstance,
328                                                                                                inventoryLocation: inventoryLocationInstance)
329                        if(!alternateItemInstance.save()) {
330                            log.error "Failed to create alternateItems on line: " + lineNumber
331                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
332                        }
333                    }
334
335                    inventoryParams.alternateItems.add(alternateItemInstance)
336                }
337
338                // spareFor.
339                tempSpareFor = parseInputList(inventoryParams.spareFor)
340                inventoryParams.spareFor = []
341
342                for(asset in tempSpareFor) {
343
344                    println ''
345                    println 'asset: '+asset
346                    println ''
347
348                    spareForInstance = Asset.findByName(asset)
349                    if(!spareForInstance) {
350                        log.error "Failed to find 'Spare For' asset on line: " + lineNumber
351                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
352                    }
353
354                    inventoryParams.spareFor.add(spareForInstance)
355                }
356
357                // Assign the retrieved or created instances to params.
358                inventoryParams.inventoryLocation = inventoryLocationInstance
359                inventoryParams.inventoryGroup = inventoryGroupInstance
360                inventoryParams.inventoryType = inventoryTypeInstance
361                inventoryParams.unitOfMeasure = unitOfMeasureInstance
362                inventoryParams.averageDeliveryPeriod = averageDeliveryPeriodInstance
363
364                // Debug
365                log.debug "InventoryParams: "
366                log.debug inventoryParams
367
368                // Create new or update.
369                inventoryItemInstance = InventoryItem.findByName(inventoryParams.name)
370                if(inventoryItemInstance) {
371                    log.info "Updating existing item: " + inventoryItemInstance
372                    inventoryItemInstance.properties = inventoryParams
373                }
374                else {
375                    log.info "Creating new item: " + inventoryParams.name
376                    inventoryItemInstance = new InventoryItem(inventoryParams)
377                }
378
379                // Save inventoryItem.
380                if(inventoryItemInstance.hasErrors() || !inventoryItemInstance.save()) {
381                    log.error "Failed to create item on line: " + column + "(" + lineNumber + ")"
382                    log.debug inventoryItemInstance.errors
383                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
384                }
385
386                if(lineNumber % 100 == 0)
387                    cleanUpGorm()
388
389                if(!result.error) nextLine()
390            } //while(line)
391
392            // Success.
393            log.info "End of file."
394            reader.close()
395            return result
396
397        } //end withTransaction
398    } // end importInventory()
399
400    /**
401    * Build an inventory template csv file.
402    * This template can then be populated for import.
403    * @returns The template as a String in csv format.
404    */
405    def buildInventoryTemplate() {
406
407        StringWriter sw = new StringWriter()
408        CSVWriter writer = new CSVWriter(sw)
409
410        writeTemplateLines(writer)
411
412        writer.close()
413        return sw.toString()
414    }
415
416    private writeTemplateLines(writer) {
417        writer.writeNext(templateHeaderLine1 as String[])
418        writer.writeNext()
419        writer.writeNext("Comment: The header line is required.")
420        writer.writeNext("Comment: Required columns are marked with a (*) in the header line.")
421        writer.writeNext("Comment: Lists of items in a column must be separated by a semicolon (;), not a comma.")
422        writer.writeNext("Comment: The at symbol (@) is reserved for indicating supplier and manufacturer types.")
423        writer.writeNext("Comment: Identical and existing names will be considered as the same item.")
424        writer.writeNext("Comment: Lines containing 'comment' will be ignored.")
425        writer.writeNext("Comment: Lines containing 'example' will be ignored.")
426        writer.writeNext("Comment: This file must be saved as a CSV file before import.")
427        writer.writeNext()
428    }
429
430    /**
431    * Build an inventory example/test file.
432    * This test file can be imported to test the import and export methods.
433    * @returns The test file as a String in csv format.
434    */
435    def buildInventoryExample() {
436
437        StringWriter sw = new StringWriter()
438        CSVWriter writer = new CSVWriter(sw)
439
440        writeTemplateLines(writer)
441
442        // Requires creation of some of the base/group/type data.
443        writer.writeNext(["Split19", "19mm split pin", "Very usefull item.",
444                                        "1024", "0", "1",
445                                        "each", "5", "NZD",
446                                        "false", "BR4",
447                                        "Store #99", "Inventory Depot",
448                                        "Mechanical Stock",
449                                        "Consumable",
450                                        "7", "Week(s)",
451                                        "123", "Multi Distributors1@OEM; Multi Distributors2@Local",
452                                        "321", "Mega Manufacturer1@OEM;Mega Manufacturer2@Alternate",
453                                        "2204E-2RS", ""
454                                        ] as String[])
455
456        // Using existing base data.
457        writer.writeNext(["2204E-2RS", "Double Row Self Align Ball Bearing 2204E-2RS - Sealed - 20/47x18", "",
458                                        "4", "1", "9",
459                                        "each", "16.35", "USD",
460                                        "TRUE", "BR4",
461                                        "Store #99", "Inventory Depot",
462                                        "Mechanical Stock",
463                                        "Consumable",
464                                        "2", "Month(s)",
465                                        "456KL", "Multi Distributors1; Multi Distributors2",
466                                        "654OP", "Mega Manufacturer1;Mega Manufacturer2",
467                                        "", ""
468                                        ] as String[])
469
470        writer.close()
471        return sw.toString()
472    }
473
474    /**
475    * Build complete inventory for export.
476    * @param inventoryItemList The list of inventory items to build..
477    * @returns The inventory as a String in csv format.
478    */
479    def buildInventory(List inventoryItemList) {
480
481        def sw = new StringWriter()
482        def writer = new CSVWriter(sw)
483
484        writeTemplateLines(writer)
485
486        //Rows
487        def row
488
489        inventoryItemList.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { inventoryItem ->
490            row = []
491            row.add(inventoryItem.name)
492            row.add(inventoryItem.description)
493            row.add(inventoryItem.comment)
494            row.add(inventoryItem.unitsInStock)
495            row.add(inventoryItem.reorderPoint)
496            row.add(inventoryItem.recommendedReorderPoint)
497            row.add(inventoryItem.unitOfMeasure)
498            row.add(inventoryItem.estimatedUnitPriceAmount)
499            row.add(inventoryItem.estimatedUnitPriceCurrency)
500            row.add(inventoryItem.enableReorder)
501            row.add(inventoryItem.inventoryLocation)
502            row.add(inventoryItem.inventoryLocation.inventoryStore)
503            row.add(inventoryItem.inventoryLocation.inventoryStore.site)
504            row.add(inventoryItem.inventoryGroup)
505            row.add(inventoryItem.inventoryType)
506            row.add(inventoryItem.averageDeliveryTime)
507            row.add(inventoryItem.averageDeliveryPeriod)
508            row.add(inventoryItem.suppliersPartNumber)
509
510            row.add( inventoryItem.suppliers.sort { p1, p2 ->
511                p1.name.compareToIgnoreCase(p2.name)
512            }.collect { it.name + "@" + it.supplierType }.join(';') )
513
514            row.add(inventoryItem.manufacturersPartNumber)
515
516            row.add(inventoryItem.manufacturers.sort { p1, p2 ->
517                p1.name.compareToIgnoreCase(p2.name)
518            }.collect { it.name + "@" + it.manufacturerType }.join(';'))
519
520            row.add(inventoryItem.alternateItems.sort { p1, p2 ->
521                p1.name.compareToIgnoreCase(p2.name)
522            }.collect { it.name }.join(';') )
523
524            row.add(inventoryItem.spareFor.sort { p1, p2 ->
525                p1.name.compareToIgnoreCase(p2.name)
526            }.collect { it.name }.join(';'))
527
528            writer.writeNext(row as String[])
529        }
530
531        writer.close()
532        return sw.toString()
533    } // end buildInventory
534
535    private getTemplateHeaderLine1() {
536            ["Name*", "Description", "Comment", "Units In Stock", "Reorder Point*", "Recommended Reorder Point", "Unit Of Measure*",
537            "Estimated Unit Price", "Currency", "Enable Reorder", "Location*", "Store*", "Site*", "Group*", "Type*",
538            "averageDeliveryTime", "averageDeliveryPeriod", "Supplier's Part Number", "Supplier",
539            "Manufacturer's Part Number", "Manufacturer", "Alternate Item", "Spare For"]
540    }
541
542    /**
543    * This cleans up the hibernate session and a grails map.
544    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
545    * The hibernate session flush is normal for hibernate.
546    * The map is apparently used by grails for domain object validation errors.
547    * A starting point for clean up is every 100 objects.
548    */
549    def cleanUpGorm() {
550        def session = sessionFactory.currentSession
551        session.flush()
552        session.clear()
553        propertyInstanceMap.get().clear()
554    }
555
556} // end class
Note: See TracBrowser for help on using the repository browser.