source: trunk/grails-app/services/InventoryItemService.groovy @ 727

Last change on this file since 727 was 727, checked in by gav, 9 years ago

Improvements to InventoryItem views.

File size: 17.5 KB
Line 
1import org.codehaus.groovy.grails.commons.ConfigurationHolder
2import org.apache.commons.lang.WordUtils
3
4/**
5* Provides a service class for the InventoryItem domain class.
6*/
7class InventoryItemService {
8
9    boolean transactional = false
10
11    def createDataService
12
13    def sessionFactory
14    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP
15
16    /**
17    * Prepare the data for the show view.
18    * The result can be used to easily construct the model for the show view.
19    * @param params The incoming params as normally passed to the show view
20    * primarily including the id of the inventoryItem.
21    * @returns A map containing result.error, if any error, otherwise result.inventoryItemInstance.
22    */
23    def show(params) {
24        def result = [:]
25
26        def fail = { Map m ->
27            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
28            return result
29        }
30
31        result.inventoryItemInstance = InventoryItem.get( params.id )
32
33        if(!result.inventoryItemInstance)
34            return fail(code:"default.not.found")
35
36        def p = [:]
37
38        if(params.paginate == "purchases") {
39            params.showTab = "showPurchasingTab"
40            p.max = Math.min(params.max?.toInteger() ?: 10, 100)
41            p.offset = params.offset?.toInteger() ?: 0
42            p.sort = params.sort ?: null
43            p.order = params.order ?: null
44        }
45        else {
46            p.max = 10
47            p.offset = 0
48        }
49
50        result.inventoryItemPurchasesTotal = InventoryItemPurchase.countByInventoryItem(result.inventoryItemInstance)
51
52        result.inventoryItemPurchases = InventoryItemPurchase.withCriteria {
53                eq("inventoryItem", result.inventoryItemInstance)
54                maxResults(p.max)
55                firstResult(p.offset)
56                // Sorting:
57                // Default is to sort by order number then id.
58                // When a sortable column is clicked then we sort by that.
59                // If the sortable column clicked is order number then we add id as the second sort.
60                if(p.sort && p.order) {
61                    order(p.sort, p.order)
62                    if(p.sort == "purchaseOrderNumber") order('id', 'asc')
63                }
64                else {
65                    order('purchaseOrderNumber', 'desc')
66                    order('id', 'asc')
67                }
68            }
69
70        result.showTab = [:]
71        switch (params.showTab) {
72            case "showMovementTab":
73                result.showTab.movement =  new String("true")
74                break
75            case "showPurchasingTab":
76                result.showTab.purchasing =  new String("true")
77                break
78            default:
79                result.showTab.inventory = new String("true")
80        }
81
82        p.max = result.inventoryMovementListMax = 10
83        p.offset = 0
84        p.order = "desc"
85        p.sort = "id"
86        result.inventoryMovementList = InventoryMovement.findAllByInventoryItem(result.inventoryItemInstance, p)
87        result.inventoryMovementListTotal = InventoryMovement.countByInventoryItem(result.inventoryItemInstance)
88
89
90        // Success.
91        return result
92
93    } // end show()
94
95    def delete(params) {
96        InventoryItem.withTransaction { status ->
97            def result = [:]
98
99            def fail = { Map m ->
100                status.setRollbackOnly()
101                if(result.inventoryItemInstance && m.field)
102                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
103                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
104                return result
105            }
106
107            result.inventoryItemInstance = InventoryItem.get(params.id)
108
109            if(!result.inventoryItemInstance)
110                return fail(code:"default.not.found")
111
112            if(result.inventoryItemInstance.inventoryMovements)
113                return fail(code:"inventoryMovement.still.associated")
114
115            try {
116                result.inventoryItemInstance.delete(flush:true)
117                return result //Success.
118            }
119            catch(org.springframework.dao.DataIntegrityViolationException e) {
120                return fail(code:"default.delete.failure")
121            }
122
123        } //end withTransaction
124    } // end delete()
125
126    def edit(params) {
127        def result = [:]
128        def fail = { Map m ->
129            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
130            return result
131        }
132
133        result.inventoryItemInstance = InventoryItem.get(params.id)
134
135        if(!result.inventoryItemInstance)
136            return fail(code:"default.not.found")
137
138        // Success.
139        return result
140    }
141
142    def update(params) {
143        InventoryItem.withTransaction { status ->
144            def result = [:]
145
146            def fail = { Map m ->
147                status.setRollbackOnly()
148                if(result.inventoryItemInstance && m.field)
149                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
150                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
151                return result
152            }
153
154            result.inventoryItemInstance = InventoryItem.get(params.id)
155
156            if(!result.inventoryItemInstance)
157                return fail(code:"default.not.found")
158
159            // Optimistic locking check.
160            if(params.version) {
161                if(result.inventoryItemInstance.version > params.version.toLong())
162                    return fail(field:"version", code:"default.optimistic.locking.failure")
163            }
164
165            result.inventoryItemInstance.properties = params
166            result.inventoryItemInstance.setAlternateSuppliersFromCheckBoxList(params.alternateSuppliers)
167            result.inventoryItemInstance.setSpareForFromCheckBoxList(params.spareFor)
168
169            // Fetch to prevent lazy initialization error.
170            result.inventoryItemInstance.unitOfMeasure
171
172            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
173                return fail(code:"default.update.failure")
174
175            // Success.
176            return result
177
178        } //end withTransaction
179    }  // end update()
180
181    def create(params) {
182        def result = [:]
183        def fail = { Map m ->
184            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
185            return result
186        }
187
188        result.inventoryItemInstance = new InventoryItem()
189        result.inventoryItemInstance.properties = params
190
191        // success
192        return result
193    }
194
195    def save(params) {
196        InventoryItem.withTransaction { status ->
197            def result = [:]
198
199            def fail = { Map m ->
200                status.setRollbackOnly()
201                if(result.inventoryItemInstance && m.field)
202                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
203                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
204                return result
205            }
206
207            result.inventoryItemInstance = new InventoryItem(params)
208            result.inventoryItemInstance.setAlternateSuppliersFromCheckBoxList(params.alternateSuppliers)
209            result.inventoryItemInstance.setSpareForFromCheckBoxList(params.spareFor)
210
211            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
212                return fail(code:"default.create.failure")
213
214            // success
215            return result
216
217        } //end withTransaction
218    }
219
220    /**
221    * Save an inventory item picture.
222    * @param params An object or map containing at least the inventoryItem ID.
223    * @param pictureSource A supported source to get the picture image from.
224    * Supported sources:
225    * HttpServletRequest e.g: 'request' var from controller to run getFile('file') against.
226    * ServletContextResource e.g: grailsApplication.mainContext.getResource('images/logo.png')
227    * File e.g: new File('picture.jpg')
228    */
229    def savePicture(params, pictureSource) {
230        InventoryItem.withTransaction { status ->
231            def result = [:]
232
233            def kByteMultiplier = 1000
234
235            def fail = { Map m ->
236                status.setRollbackOnly()
237                if(result.inventoryItemInstance && m.field)
238                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
239                result.error = [ code: m.code, args: m.args ?: ["InventoryItem", params.id] ]
240                return result
241            }
242
243            result.inventoryItemInstance = InventoryItem.get(params.id)
244
245            if(!result.inventoryItemInstance)
246                return fail(code:"default.not.found")
247
248            // Optimistic locking check.
249            if(params.version) {
250                if(result.inventoryItemInstance.version > params.version.toLong())
251                    return fail(field:"version", code:"default.optimistic.locking.failure")
252            }
253
254            if(result.inventoryItemInstance.picture)
255                return fail(field:"picture", code:"inventory.item.already.has.picture")
256
257            // Declare some more variables, since we appear to have most of what we need.
258            def picture = new Picture(inventoryItem: result.inventoryItemInstance)
259            def imaging = new Imaging()
260            def images = null
261            def pictureFile
262            def pictureFileName = ''
263            def pictureInputStream
264
265            // Check the supplied pictureSource and get the inputStream.
266            if(pictureSource instanceof javax.servlet.http.HttpServletRequest) {
267                def multiPartFile = pictureSource.getFile('file')
268                pictureFileName = multiPartFile.originalFilename
269
270                if(!multiPartFile || multiPartFile.isEmpty())
271                    return fail(code: "default.file.not.supplied")
272
273                if (multiPartFile.getSize() > Image.MAX_SIZE)
274                    return fail(code: "default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
275
276                pictureInputStream = multiPartFile.inputStream
277            }
278            else if(pictureSource instanceof org.springframework.web.context.support.ServletContextResource) {
279                pictureFile = pictureSource.getFile()
280                pictureFileName = pictureFile.name
281
282                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
283                    return fail(code:"default.file.not.supplied")
284
285                if (pictureFile.length() > Image.MAX_SIZE)
286                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
287
288                pictureInputStream = pictureSource.inputStream
289            }
290            else if(pictureSource instanceof File) {
291                pictureFile = pictureSource
292                pictureFileName = pictureFile.name
293
294                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
295                    return fail(code:"default.file.not.supplied")
296
297                if (pictureFile.length() > Image.MAX_SIZE)
298                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])
299
300                pictureInputStream = new FileInputStream(pictureSource)
301            }
302            else {
303                    return fail(code:"inventory.item.picture.source.not.supported")
304            }
305
306            // Create the Images.
307            try {
308                images = imaging.createAll(result.inventoryItemInstance, picture, pictureInputStream)
309                // Ensure the stream is closed.
310                pictureInputStream.close()
311            }
312            catch(Exception ex) {
313                log.error("picture save", ex)
314                // Ensure the stream is closed.
315                pictureInputStream.close()
316                return fail(code:"inventory.item.picture.file.unrecognised", args: [pictureFileName])
317            }
318
319            // Add images to picture.
320            images.each { image ->
321                picture.addToImages(image)
322            }
323
324            // Save picture.
325            if(picture.hasErrors() || !picture.save())
326                return fail(code:"default.create.failure", args: ["Picture"])
327
328            result.inventoryItemInstance.picture = picture
329
330            // Save inventoryItem.
331            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
332                return fail(code:"default.create.failure")
333
334            // success
335            return result
336
337        } // end withTransaction
338    } // savePicture
339
340    /**
341    * Import inventory pictures from an uploaded zip file or picture.
342    * @param request The http request to run getFile against.
343    * Get file should return a zip format file containing the inventory item pictures or a picture file.
344    */
345    def importInventoryItemPictures(request) {
346            def result = [:]
347
348            def kByteMultiplier = 1000
349            def mByteMultiplier = 1000 * kByteMultiplier
350            def fileMaxSize = 100 * mByteMultiplier
351
352            def fail = { Map m ->
353                result.error = [ code: m.code, args: m.args ]
354                return result
355            }
356
357            // Get file from request.
358            def multiPartFile = request.getFile('file')
359            def uploadedFileName = multiPartFile.originalFilename
360
361            if(!multiPartFile || multiPartFile.isEmpty())
362                return fail(code: "default.file.not.supplied")
363
364            if (multiPartFile.getSize() > fileMaxSize)
365                return fail(code: "default.file.over.max.size", args: [fileMaxSize/mByteMultiplier, "MB"])
366
367            // Check and create import dir.
368            def dir = new File(ConfigurationHolder.config.globalDirs.tempInventoryItemPicturesDirectory)
369
370            if(!dir.exists())
371                dir.mkdirs()
372
373            if(!dir.isDirectory()) {
374                return fail(code:'inventoryItemPictures.import.failure.no.directory')
375            }
376
377            // Write file to disk.
378            def diskFile = new File(dir.absolutePath + File.separator + uploadedFileName)
379            multiPartFile.transferTo(diskFile)
380
381            // File patterns
382            def zipFilePattern = ~/[^\s].*(\.(?i)(zip))$/
383            def pictureFilePattern = ~/[^\s].*(\.(?i)(jpg|png|gif|bmp))$/
384
385            // If file claims to be a zip file then try using ant to unzip.
386            if(diskFile.name.matches(zipFilePattern)) {
387                def ant = new AntBuilder()
388                try {
389                    ant.unzip(  src: diskFile.absolutePath,
390                                        dest: dir.absolutePath,
391                                        overwrite:"true" )
392                }
393                catch(e) {
394                    log.error e
395                    return fail(code:'inventoryItemPictures.import.failure.to.unzip')
396                }
397            }
398
399            // Recurse through dir building list of pictureFiles.
400            def pictureFiles = []
401            dir.eachFileMatch(pictureFilePattern) {
402                pictureFiles << it
403            }
404
405            dir.eachDirRecurse { subDir ->
406                subDir.eachFileMatch(pictureFilePattern) {
407                    pictureFiles << it
408                }
409            }
410
411            pictureFiles.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }
412
413            // Find inventoryItems by name of picture and call savePicture.
414            def inventoryItemInstance
415            def itemName
416            def savePictureResult
417            def pictureCount = 0
418            def picturesSavedCount = 0
419
420            // Turn off index mirroring.
421            createDataService.stopSearchableIndex()
422
423            for(pictureFile in pictureFiles) {
424                pictureCount++
425
426                if(pictureCount % 10 == 0) {
427                    cleanUpGorm()
428                }
429
430                itemName = WordUtils.capitalize(pictureFile.name[0..-5])
431                inventoryItemInstance = InventoryItem.findByName(itemName)
432                if(!inventoryItemInstance) {
433                    log.warn 'InventoryItem not found with name: ' + itemName
434                    continue
435                }
436                if(inventoryItemInstance.picture) {
437                    log.warn 'InventoryItem already has picture: ' + itemName
438                    continue
439                }
440                savePictureResult = savePicture(inventoryItemInstance, pictureFile)
441                if(savePictureResult.error)
442                    log.error savePictureResult.error
443                else {
444                    picturesSavedCount++
445                    log.info 'InventoryItem picture saved: ' + itemName
446                }
447            }
448
449            // Start mirroring again and rebuild index.
450            createDataService.startSearchableIndex()
451
452            log.info 'InventoryItem pictures saved: ' + picturesSavedCount
453            log.info 'InventoryItem pictures total: ' + pictureCount
454
455            // Cleanup.
456            dir.eachFile() {
457                if(it.isDirectory())
458                    it.deleteDir()
459                else
460                    it.delete()
461            }
462
463            // Success.
464            return result
465
466    } // importInventoryItemPictures
467
468    /**
469    * This cleans up the hibernate session and a grails map.
470    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
471    * The hibernate session flush is normal for hibernate.
472    * The map is apparently used by grails for domain object validation errors.
473    * A starting point for clean up is every 100 objects.
474    */
475    def cleanUpGorm() {
476        def session = sessionFactory.currentSession
477        session.flush()
478        session.clear()
479        propertyInstanceMap.get().clear()
480    }
481
482} // end class
Note: See TracBrowser for help on using the repository browser.