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

Last change on this file since 644 was 633, checked in by gav, 14 years ago

Domain change: Add PurchasingGroup?.
Logic and views to suite.

File size: 19.6 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 puchase 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            result.inventoryItemPurchaseInstance.properties = params
268            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
269            result.inventoryItemPurchaseInstance.lastUpdatedBy = authService.currentUser
270
271            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
272                return fail(code:"default.update.failure")
273
274            // Success.
275            return result
276
277        } //end withTransaction
278    }  // end update()
279
280    def save(params) {
281        InventoryItemPurchase.withTransaction { status ->
282            def result = [:]
283            def fail = { Map m ->
284                status.setRollbackOnly()
285                if(result.inventoryItemPurchase && m.field)
286                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
287                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
288                return result
289            }
290
291            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
292            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
293            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
294            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(1) // Order
295
296            // Fetch to prevent lazy initialization error.
297            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
298
299            // Prevent ordering on obsolete or inactive inventoryItem.
300            def isObsolete = result.inventoryItemPurchaseInstance.inventoryItem?.isObsolete
301            def isActive = result.inventoryItemPurchaseInstance.inventoryItem?.isActive
302            if(isObsolete || !isActive)
303                return fail(code:"inventoryItemPurchase.operation.not.permitted.on.inactive.or.obsolete.item")
304
305            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
306                return fail(code:"default.create.failure")
307
308            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
309
310            // success
311            return result
312
313        } // end withTransaction
314    } // save()
315
316    def receiveSave(params) {
317        InventoryItemPurchase.withTransaction { status ->
318            def result = [:]
319
320            def fail = { Map m ->
321                status.setRollbackOnly()
322                if(result.inventoryItemPurchase && m.field)
323                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
324                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
325                return result
326            }
327
328            def order = InventoryItemPurchase.get(params.orderId)
329            if(!order)
330                return fail(code:"default.not.found")
331            result.orderId = order.id
332
333            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
334            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
335            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
336            result.inventoryItemPurchaseInstance.costCode = order.costCode
337            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
338
339            def calcQuantities = calcQuantities(order)
340            if(result.inventoryItemPurchaseInstance.quantity)
341                calcQuantities.totalReceived += result.inventoryItemPurchaseInstance.quantity
342            if(result.inventoryItemPurchaseInstance.orderValueAmount)
343                calcQuantities.totalReceivedAmount += result.inventoryItemPurchaseInstance.orderValueAmount
344
345            if(calcQuantities.totalReceived >= calcQuantities.totalOrdered) {
346                order.receivedComplete = true
347                result.inventoryItemPurchaseInstance.receivedComplete = true
348                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(3) // Received Complete.
349            }
350            else {
351                order.receivedComplete = false
352                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(2) // Received B/oder to Come.
353            }
354
355            // Fetch to prevent lazy initialization error.
356            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
357            result.inventoryItemPurchaseInstance.inventoryItem.inventoryLocation
358
359            if(order.hasErrors() || !order.save())
360                return fail(code:"default.create.failure")
361
362            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
363                return fail(code:"default.create.failure")
364
365            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
366
367            // Perform the inventory movement.
368            if(result.inventoryItemPurchaseInstance.quantity > 0) {
369                def p = [:]
370                p.inventoryItem = result.inventoryItemPurchaseInstance.inventoryItem
371                p.quantity = result.inventoryItemPurchaseInstance.quantity
372                p.inventoryMovementType = InventoryMovementType.read(3)
373                def movementResult = inventoryMovementService.move(p)
374                if(movementResult.error)
375                    return fail(code:"default.create.failure")
376            }
377
378            // success
379            return result
380
381        } // end withTransaction
382    } // receiveSave()
383
384    def approveInvoicePaymentSave(params) {
385        InventoryItemPurchase.withTransaction { status ->
386            def result = [:]
387
388            def fail = { Map m ->
389                status.setRollbackOnly()
390                if(result.inventoryItemPurchaseInstance && m.field)
391                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
392                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
393                return result
394            }
395
396            def received = InventoryItemPurchase.get(params.receivedId)
397            if(!received)
398                return fail(code:"default.not.found")
399            result.receivedId = received.id
400
401            def order = getOriginalOrder(received)
402            if(!order)
403                return fail(code:"default.not.found")
404            result.orderId = order.id
405
406            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
407            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
408            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
409            result.inventoryItemPurchaseInstance.costCode = order.costCode
410            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
411            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(4) // Approve.
412
413            received.invoicePaymentApproved = true
414            result.inventoryItemPurchaseInstance.invoicePaymentApproved = true
415
416            // Update orderValueAmount and invoicePaymentApproved if processing a receivedComplete.
417            if(received.inventoryItemPurchaseType.id == 3L) {
418                order.invoicePaymentApproved = true
419                result.inventoryItemPurchaseInstance.receivedComplete = true
420                def calcQuantities = calcQuantities(order)
421                if(result.inventoryItemPurchaseInstance.orderValueAmount)
422                    calcQuantities.totalPaymentApproved += result.inventoryItemPurchaseInstance.orderValueAmount
423                order.orderValueAmount = calcQuantities.totalPaymentApproved
424            }
425
426            // Fetch to prevent lazy initialization error.
427            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
428
429            if(!result.inventoryItemPurchaseInstance.invoiceNumber)
430                return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
431
432            if(order.hasErrors() || !order.save())
433                return fail(code:"default.create.failure")
434
435            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
436                return fail(code:"default.create.failure")
437
438            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
439
440            // Success..
441            return result
442
443        } // end withTransaction
444    } // approveInvoicePaymentSave()
445
446} // end class
Note: See TracBrowser for help on using the repository browser.