source: trunk/grails-app/services/InventoryPurchaseService.groovy

Last change on this file was 964, checked in by gav, 7 years ago

Fix for ticket #105 - InventoryItemPurchase?'s can't be deleted when order quantity = 0 and allows the order to be deleted.

File size: 21.8 KB
Line 
1class InventoryPurchaseService {
2
3    boolean transactional = false
4
5    def authService
6    def dateUtilService
7    def inventoryMovementService
8
9    /**
10    * Calulates the quantities for an inventoryItem and purchaseOrderNumber.
11    * @param order An inventory purchase that was the source of the order.
12    * @returns A result map containing the totalOrdered, totalReceived, totalRemaining, thisOrderRemaining,
13    *                 totalOrderedAmount, totalReceivedAmount, totalRemainingAmount, thisOrderRemainingAmount,
14    *                 totalPaymentApproved.
15    */
16    def calcQuantities(order) {
17
18        def result = [:]
19
20        result.totalOrdered = 0
21        result.totalOrderedAmount = 0
22        result.totalReceived = 0
23        result.totalReceivedAmount = 0
24        result.totalPaymentApproved = 0
25        InventoryItemPurchase.withCriteria {
26            eq("inventoryItem", order.inventoryItem)
27            eq("purchaseOrderNumber", order.purchaseOrderNumber)
28        }.each() {
29            if(it.inventoryItemPurchaseType.id == 1L) { // Orders.
30                result.totalOrdered += it.quantity
31                result.totalOrderedAmount += it.orderValueAmount
32            }
33            if(it.inventoryItemPurchaseType.id == 2L || it.inventoryItemPurchaseType.id == 3L) { // Received B/order and Complete.
34                result.totalReceived += it.quantity
35                result.totalReceivedAmount += it.orderValueAmount
36            }
37            if(it.inventoryItemPurchaseType.id == 4L) { // Approved.
38                result.totalPaymentApproved += it.orderValueAmount
39            }
40        }
41
42        result.totalRemaining
43        if(result.totalOrdered > result.totalReceived)
44            result.totalRemaining = result.totalOrdered - result.totalReceived
45        else
46            result.totalRemaining = 0
47
48        result.totalRemainingAmount
49        if(result.totalOrderedAmount > result.totalReceivedAmount)
50            result.totalRemainingAmount = result.totalOrderedAmount - result.totalReceivedAmount
51        else
52            result.totalRemainingAmount = 0
53
54        result.thisOrderRemaining
55        if(result.totalRemaining > order.quantity)
56            result.thisOrderRemaining = order.quantity
57        else
58            result.thisOrderRemaining = result.totalRemaining
59
60        result.thisOrderRemainingAmount
61        if(result.totalRemainingAmount > order.orderValueAmount)
62            result.thisOrderRemainingAmount = order.orderValueAmount
63        else
64            result.thisOrderRemainingAmount = result.totalRemainingAmount
65
66        return result
67    }
68
69    /**
70    * Get the original order for an inventoryItemPurchase and InventoryItem.
71    * @param inventoryItemPurchase An inventory puchase.
72    * @returns The order.
73    */
74    def getOriginalOrder(inventoryItemPurchase) {
75
76        def namedParams = [:]
77
78        namedParams.inventoryItem = inventoryItemPurchase.inventoryItem
79        namedParams.purchaseOrderNumber = inventoryItemPurchase.purchaseOrderNumber
80        namedParams.orderPlaced = InventoryItemPurchaseType.read(1)
81
82        def order = InventoryItemPurchase.find("from InventoryItemPurchase as p \
83                                                                                where( p.inventoryItem = :inventoryItem \
84                                                                                            and p.purchaseOrderNumber = :purchaseOrderNumber \
85                                                                                            and p.inventoryItemPurchaseType = :orderPlaced )",
86                                                                            namedParams)
87
88        return order
89    }
90
91    /**
92    * Get costCodes by person and the purchasingGroups they have been assigned.
93    * @param person A Person, defaults to currentUser.
94    * @returns A list of CostCodes.
95    */
96    def getCostCodesByPerson(person = authService.currentUser) {
97        if(person.purchasingGroups) {
98            CostCode.withCriteria {
99                    eq('isActive', true)
100                    or {
101                        person.purchasingGroups.each() { purchasingGroup ->
102                            eq('purchasingGroup', purchasingGroup)
103                        }
104                    }
105            }.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } // withCriteria
106        }
107        else
108            []
109    } // getCostCodesByPerson
110
111    /**
112    * Check if a person is in a purchasing group.
113    * @param person A PurchasingGroup to check for.
114    * @param person A Person, defaults to currentUser.
115    * @returns True if person is in group.
116    */
117    def isPersonInPurchasingGroup(purchasingGroup, person = authService.currentUser) {
118        for(pg in person.purchasingGroups) {
119            if(pg.id == purchasingGroup.id)
120                return true
121        }
122    } // isPersonInPurchasingGroup
123
124    def delete(params) {
125        InventoryItemPurchase.withTransaction { status ->
126            def result = [:]
127            def fail = { Map m ->
128                status.setRollbackOnly()
129                if(result.inventoryItemPurchase && m.field)
130                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
131                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
132                return result
133            }
134
135            result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)
136
137            if(!result.inventoryItemPurchaseInstance)
138                return fail(code:"default.not.found")
139
140            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
141            def purchaseTypeId = result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id
142
143            // Handle Invoice Payment Approved.
144            if(purchaseTypeId == 4) {
145                // Find and mark all orders as invoicePaymentApproved = false.
146                InventoryItemPurchase.withCriteria {
147                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
148                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
149                    inventoryItemPurchaseType {
150                            eq("id", 1L) // Order Placed.
151                    }
152                }.each() {
153                    it.invoicePaymentApproved = false
154                }
155                // Find and mark last orderReceived as invoicePaymentApproved = false.
156                InventoryItemPurchase.withCriteria {
157                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
158                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
159                    inventoryItemPurchaseType {
160                        or {
161                            eq("id", 2L) // Received B/order To Come
162                            eq("id", 3L) // Received Complete
163                        }
164                    }
165                }.last().invoicePaymentApproved = false
166            }
167
168            // Handle Received.
169            // Refuse to delete if payment approved.
170            // Find and reverse the matching movement.
171            // Find and mark all orders as receivedComplete = false.
172            if(purchaseTypeId == 2 || purchaseTypeId == 3) {
173
174                def paymentAlreadyApproved = InventoryItemPurchase.withCriteria {
175                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
176                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
177                    inventoryItemPurchaseType {
178                            eq("id", 4L) // Invoice Payment Approved.
179                    }
180                }
181
182                if(paymentAlreadyApproved)
183                    return fail(code:"inventoryItemPurchase.delete.failure.payment.approved")
184
185                def startOfDay = dateUtilService.getMidnight(result.inventoryItemPurchaseInstance.date)
186                def inventoryMovements = InventoryMovement.withCriteria {
187                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem )
188                    eq("quantity", result.inventoryItemPurchaseInstance.quantity)
189                    between("date", startOfDay, startOfDay+1)
190                    inventoryMovementType {
191                        eq("id", 3L) // purchaseReceived.
192                    }
193                    order('id', 'desc') // The newest one will be first.
194                }
195
196                if(inventoryMovements[0]) {
197                    def movementResult = inventoryMovementService.reverseMove(inventoryMovements[0])
198                    if(movementResult.error)
199                        return fail(code:"inventoryItemPurchase.delete.failure.reverse.move")
200                }
201
202                InventoryItemPurchase.withCriteria {
203                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
204                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
205                    inventoryItemPurchaseType {
206                            eq("id", 1L) // Order Placed
207                    }
208                }.each() {
209                    it.receivedComplete = false
210                }
211            } // Handle Received.
212
213            // Handle Order Placed.
214            // Refuse to delete if we have received items.
215            // Deletion of received already requires payment approved to be deleted.
216            if(purchaseTypeId == 1) {
217
218                def calcQuantities = calcQuantities(result.inventoryItemPurchaseInstance)
219                if(calcQuantities.totalReceived > 0)
220                    return fail(code:"inventoryItemPurchase.delete.failure.received.exists")
221
222                // Cover the case of 0 ordered and received.
223                // Only possible to have Received Complete (3L) in this case and not B/order (2L).
224                def receivedExists = InventoryItemPurchase.withCriteria {
225                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
226                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
227                    inventoryItemPurchaseType {
228                            eq("id", 3L) // Received Complete.
229                    }
230                }
231
232                if(receivedExists)
233                    return fail(code:"inventoryItemPurchase.delete.failure.received.exists")
234            }
235
236            // Success.
237            // By this stage everything should be handled and the delete call is allowed and expected to pass.
238            result.inventoryItemPurchaseInstance.delete()
239            return result
240
241        } // end withTransaction
242    }
243
244    def edit(params) {
245        def result = [:]
246        def fail = { Map m ->
247            result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
248            return result
249        }
250
251        result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)
252
253        if(!result.inventoryItemPurchaseInstance)
254            return fail(code:"default.not.found")
255
256        // Success.
257        return result
258    }
259
260    def update(params) {
261        InventoryItemPurchase.withTransaction { status ->
262            def result = [:]
263
264            def fail = { Map m ->
265                status.setRollbackOnly()
266                if(result.inventoryItemPurchaseInstance && m.field)
267                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
268                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
269                return result
270            }
271
272            result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)
273
274            if(!result.inventoryItemPurchaseInstance)
275                return fail(code:"default.not.found")
276
277            // Optimistic locking check.
278            if(params.version) {
279                if(result.inventoryItemPurchaseInstance.version > params.version.toLong())
280                    return fail(field:"version", code:"default.optimistic.locking.failure")
281            }
282
283            def originalPaymentApprovedAmount = result.inventoryItemPurchaseInstance.orderValueAmount
284
285            result.inventoryItemPurchaseInstance.properties = params
286            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
287            result.inventoryItemPurchaseInstance.invoiceNumber = result.inventoryItemPurchaseInstance.invoiceNumber.trim()
288            result.inventoryItemPurchaseInstance.lastUpdatedBy = authService.currentUser
289
290            // Fetch to prevent lazy initialization error.
291            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
292
293            //  If processing an Invoice Approved.
294            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {
295                if(!result.inventoryItemPurchaseInstance.invoiceNumber)
296                    return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
297            }
298
299            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
300                return fail(code:"default.update.failure")
301
302            //  If processing an Invoice Approved.
303            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {
304
305                def order = getOriginalOrder(result.inventoryItemPurchaseInstance)
306                if(!order)
307                    return fail(code:"default.not.found")
308
309                // Update orderValueAmount if receivedComplete.
310                if(order.receivedComplete) {
311
312                    def calcQuantities = calcQuantities(order)
313                    order.orderValueAmount = calcQuantities.totalPaymentApproved
314
315                    if(order.hasErrors() || !order.save())
316                        return fail(code:"default.create.failure")
317                }
318
319            }
320
321            // Success.
322            return result
323
324        } //end withTransaction
325    }  // end update()
326
327    def save(params) {
328        InventoryItemPurchase.withTransaction { status ->
329            def result = [:]
330
331            def fail = { Map m ->
332                status.setRollbackOnly()
333                if(result.inventoryItemPurchase && m.field)
334                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
335                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
336                return result
337            }
338
339            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
340            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
341            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
342            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(1) // Order
343
344            // Fetch to prevent lazy initialization error.
345            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
346
347            // Prevent ordering on obsolete or inactive inventoryItem.
348            def isObsolete = result.inventoryItemPurchaseInstance.inventoryItem?.isObsolete
349            def isActive = result.inventoryItemPurchaseInstance.inventoryItem?.isActive
350            if(isObsolete || !isActive)
351                return fail(code:"inventoryItemPurchase.operation.not.permitted.on.inactive.or.obsolete.item")
352
353            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
354                return fail(code:"default.create.failure")
355
356            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
357
358            // success
359            return result
360
361        } // end withTransaction
362    } // save()
363
364    def receiveSave(params) {
365        InventoryItemPurchase.withTransaction { status ->
366            def result = [:]
367
368            def fail = { Map m ->
369                status.setRollbackOnly()
370                if(result.inventoryItemPurchase && m.field)
371                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
372                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
373                return result
374            }
375
376            def order = InventoryItemPurchase.get(params.orderId)
377            if(!order)
378                return fail(code:"default.not.found")
379            result.orderId = order.id
380
381            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
382            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
383            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
384            result.inventoryItemPurchaseInstance.costCode = order.costCode
385            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
386
387            def calcQuantities = calcQuantities(order)
388            if(result.inventoryItemPurchaseInstance.quantity)
389                calcQuantities.totalReceived += result.inventoryItemPurchaseInstance.quantity
390            if(result.inventoryItemPurchaseInstance.orderValueAmount)
391                calcQuantities.totalReceivedAmount += result.inventoryItemPurchaseInstance.orderValueAmount
392
393            if(calcQuantities.totalReceived >= calcQuantities.totalOrdered) {
394                order.receivedComplete = true
395                result.inventoryItemPurchaseInstance.receivedComplete = true
396                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(3) // Received Complete.
397            }
398            else {
399                order.receivedComplete = false
400                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(2) // Received B/oder to Come.
401            }
402
403            // Fetch to prevent lazy initialization error.
404            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
405            result.inventoryItemPurchaseInstance.inventoryItem.inventoryLocation
406
407            if(order.hasErrors() || !order.save())
408                return fail(code:"default.create.failure")
409
410            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
411                return fail(code:"default.create.failure")
412
413            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
414
415            // Perform the inventory movement.
416            if(result.inventoryItemPurchaseInstance.quantity > 0) {
417                def p = [:]
418                p.inventoryItem = result.inventoryItemPurchaseInstance.inventoryItem
419                p.quantity = result.inventoryItemPurchaseInstance.quantity
420                p.inventoryMovementType = InventoryMovementType.read(3)
421                def movementResult = inventoryMovementService.move(p)
422                if(movementResult.error)
423                    return fail(code:"default.create.failure")
424            }
425
426            // success
427            return result
428
429        } // end withTransaction
430    } // receiveSave()
431
432    def approveInvoicePaymentSave(params) {
433        InventoryItemPurchase.withTransaction { status ->
434            def result = [:]
435
436            def fail = { Map m ->
437                status.setRollbackOnly()
438                if(result.inventoryItemPurchaseInstance && m.field)
439                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
440                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
441                return result
442            }
443
444            def received = InventoryItemPurchase.get(params.receivedId)
445            if(!received)
446                return fail(code:"default.not.found")
447            result.receivedId = received.id
448
449            def order = getOriginalOrder(received)
450            if(!order)
451                return fail(code:"default.not.found")
452            result.orderId = order.id
453
454            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
455            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
456            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
457            result.inventoryItemPurchaseInstance.costCode = order.costCode
458            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
459            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(4) // Approve.
460
461            received.invoicePaymentApproved = true
462            result.inventoryItemPurchaseInstance.invoicePaymentApproved = true
463
464            // Update orderValueAmount and invoicePaymentApproved if processing a receivedComplete.
465            if(received.inventoryItemPurchaseType.id == 3L) {
466                order.invoicePaymentApproved = true
467                result.inventoryItemPurchaseInstance.receivedComplete = true
468                def calcQuantities = calcQuantities(order)
469                if(result.inventoryItemPurchaseInstance.orderValueAmount)
470                    calcQuantities.totalPaymentApproved += result.inventoryItemPurchaseInstance.orderValueAmount
471                order.orderValueAmount = calcQuantities.totalPaymentApproved
472            }
473
474            // Fetch to prevent lazy initialization error.
475            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
476
477            if(!result.inventoryItemPurchaseInstance.invoiceNumber)
478                return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
479
480            if(order.hasErrors() || !order.save())
481                return fail(code:"default.create.failure")
482
483            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
484                return fail(code:"default.create.failure")
485
486            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
487
488            // Success..
489            return result
490
491        } // end withTransaction
492    } // approveInvoicePaymentSave()
493
494} // end class
Note: See TracBrowser for help on using the repository browser.