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

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

Domain change: as per ticket #96 - Remove unused fields from InventoryItem?.
Removed InventoryItem?.alternateItems.

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