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

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

Fix for ticket #91 - Purchasing - run order price update when invoice is updated.

File size: 21.0 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                def movementResult = inventoryMovementService.reverseMove(inventoryMovements[0])
197                if(movementResult.error)
198                    return fail(code:"inventoryMovement.quantity.insufficientItemsInStock")
199
200                InventoryItemPurchase.withCriteria {
201                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
202                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
203                    inventoryItemPurchaseType {
204                            eq("id", 1L) // Order Placed
205                    }
206                }.each() {
207                    it.receivedComplete = false
208                }
209            } // Handle Received.
210
211            // Handle Order Placed.
212            // Refuse to delete if we have received items.
213            // Deletion of received already requires payment approved to be deleted.
214            if(purchaseTypeId == 1) {
215                def calcQuantities = calcQuantities(result.inventoryItemPurchaseInstance)
216                if(calcQuantities.totalReceived > 0)
217                    return fail(code:"inventoryItemPurchase.delete.failure.received.exists")
218            }
219
220            // Success.
221            // By this stage everything should be handled and the delete call is allowed and expected to pass.
222            result.inventoryItemPurchaseInstance.delete()
223            return result
224
225        } // end withTransaction
226    }
227
228    def edit(params) {
229        def result = [:]
230        def fail = { Map m ->
231            result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
232            return result
233        }
234
235        result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)
236
237        if(!result.inventoryItemPurchaseInstance)
238            return fail(code:"default.not.found")
239
240        // Success.
241        return result
242    }
243
244    def update(params) {
245        InventoryItemPurchase.withTransaction { status ->
246            def result = [:]
247
248            def fail = { Map m ->
249                status.setRollbackOnly()
250                if(result.inventoryItemPurchaseInstance && m.field)
251                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
252                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
253                return result
254            }
255
256            result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)
257
258            if(!result.inventoryItemPurchaseInstance)
259                return fail(code:"default.not.found")
260
261            // Optimistic locking check.
262            if(params.version) {
263                if(result.inventoryItemPurchaseInstance.version > params.version.toLong())
264                    return fail(field:"version", code:"default.optimistic.locking.failure")
265            }
266
267            def originalPaymentApprovedAmount = result.inventoryItemPurchaseInstance.orderValueAmount
268
269            result.inventoryItemPurchaseInstance.properties = params
270            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
271            result.inventoryItemPurchaseInstance.invoiceNumber = result.inventoryItemPurchaseInstance.invoiceNumber.trim()
272            result.inventoryItemPurchaseInstance.lastUpdatedBy = authService.currentUser
273
274            // Fetch to prevent lazy initialization error.
275            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
276
277            //  If processing an Invoice Approved.
278            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {
279                if(!result.inventoryItemPurchaseInstance.invoiceNumber)
280                    return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
281            }
282
283            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
284                return fail(code:"default.update.failure")
285
286            //  If processing an Invoice Approved.
287            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {
288
289                def order = getOriginalOrder(result.inventoryItemPurchaseInstance)
290                if(!order)
291                    return fail(code:"default.not.found")
292
293                // Update orderValueAmount if receivedComplete.
294                if(order.receivedComplete) {
295
296                    def calcQuantities = calcQuantities(order)
297                    order.orderValueAmount = calcQuantities.totalPaymentApproved
298
299                    if(order.hasErrors() || !order.save())
300                        return fail(code:"default.create.failure")
301                }
302
303            }
304
305            // Success.
306            return result
307
308        } //end withTransaction
309    }  // end update()
310
311    def save(params) {
312        InventoryItemPurchase.withTransaction { status ->
313            def result = [:]
314
315            def fail = { Map m ->
316                status.setRollbackOnly()
317                if(result.inventoryItemPurchase && m.field)
318                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
319                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
320                return result
321            }
322
323            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
324            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
325            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
326            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(1) // Order
327
328            // Fetch to prevent lazy initialization error.
329            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
330
331            // Prevent ordering on obsolete or inactive inventoryItem.
332            def isObsolete = result.inventoryItemPurchaseInstance.inventoryItem?.isObsolete
333            def isActive = result.inventoryItemPurchaseInstance.inventoryItem?.isActive
334            if(isObsolete || !isActive)
335                return fail(code:"inventoryItemPurchase.operation.not.permitted.on.inactive.or.obsolete.item")
336
337            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
338                return fail(code:"default.create.failure")
339
340            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
341
342            // success
343            return result
344
345        } // end withTransaction
346    } // save()
347
348    def receiveSave(params) {
349        InventoryItemPurchase.withTransaction { status ->
350            def result = [:]
351
352            def fail = { Map m ->
353                status.setRollbackOnly()
354                if(result.inventoryItemPurchase && m.field)
355                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
356                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
357                return result
358            }
359
360            def order = InventoryItemPurchase.get(params.orderId)
361            if(!order)
362                return fail(code:"default.not.found")
363            result.orderId = order.id
364
365            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
366            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
367            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
368            result.inventoryItemPurchaseInstance.costCode = order.costCode
369            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
370
371            def calcQuantities = calcQuantities(order)
372            if(result.inventoryItemPurchaseInstance.quantity)
373                calcQuantities.totalReceived += result.inventoryItemPurchaseInstance.quantity
374            if(result.inventoryItemPurchaseInstance.orderValueAmount)
375                calcQuantities.totalReceivedAmount += result.inventoryItemPurchaseInstance.orderValueAmount
376
377            if(calcQuantities.totalReceived >= calcQuantities.totalOrdered) {
378                order.receivedComplete = true
379                result.inventoryItemPurchaseInstance.receivedComplete = true
380                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(3) // Received Complete.
381            }
382            else {
383                order.receivedComplete = false
384                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(2) // Received B/oder to Come.
385            }
386
387            // Fetch to prevent lazy initialization error.
388            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
389            result.inventoryItemPurchaseInstance.inventoryItem.inventoryLocation
390
391            if(order.hasErrors() || !order.save())
392                return fail(code:"default.create.failure")
393
394            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
395                return fail(code:"default.create.failure")
396
397            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
398
399            // Perform the inventory movement.
400            if(result.inventoryItemPurchaseInstance.quantity > 0) {
401                def p = [:]
402                p.inventoryItem = result.inventoryItemPurchaseInstance.inventoryItem
403                p.quantity = result.inventoryItemPurchaseInstance.quantity
404                p.inventoryMovementType = InventoryMovementType.read(3)
405                def movementResult = inventoryMovementService.move(p)
406                if(movementResult.error)
407                    return fail(code:"default.create.failure")
408            }
409
410            // success
411            return result
412
413        } // end withTransaction
414    } // receiveSave()
415
416    def approveInvoicePaymentSave(params) {
417        InventoryItemPurchase.withTransaction { status ->
418            def result = [:]
419
420            def fail = { Map m ->
421                status.setRollbackOnly()
422                if(result.inventoryItemPurchaseInstance && m.field)
423                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
424                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
425                return result
426            }
427
428            def received = InventoryItemPurchase.get(params.receivedId)
429            if(!received)
430                return fail(code:"default.not.found")
431            result.receivedId = received.id
432
433            def order = getOriginalOrder(received)
434            if(!order)
435                return fail(code:"default.not.found")
436            result.orderId = order.id
437
438            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
439            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
440            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
441            result.inventoryItemPurchaseInstance.costCode = order.costCode
442            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
443            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(4) // Approve.
444
445            received.invoicePaymentApproved = true
446            result.inventoryItemPurchaseInstance.invoicePaymentApproved = true
447
448            // Update orderValueAmount and invoicePaymentApproved if processing a receivedComplete.
449            if(received.inventoryItemPurchaseType.id == 3L) {
450                order.invoicePaymentApproved = true
451                result.inventoryItemPurchaseInstance.receivedComplete = true
452                def calcQuantities = calcQuantities(order)
453                if(result.inventoryItemPurchaseInstance.orderValueAmount)
454                    calcQuantities.totalPaymentApproved += result.inventoryItemPurchaseInstance.orderValueAmount
455                order.orderValueAmount = calcQuantities.totalPaymentApproved
456            }
457
458            // Fetch to prevent lazy initialization error.
459            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
460
461            if(!result.inventoryItemPurchaseInstance.invoiceNumber)
462                return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
463
464            if(order.hasErrors() || !order.save())
465                return fail(code:"default.create.failure")
466
467            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
468                return fail(code:"default.create.failure")
469
470            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
471
472            // Success..
473            return result
474
475        } // end withTransaction
476    } // approveInvoicePaymentSave()
477
478} // end class
Note: See TracBrowser for help on using the repository browser.