class InventoryPurchaseService {

    boolean transactional = false

    def authService
    def dateUtilService
    def inventoryMovementService

    /**
    * Calulates the quantities for an inventoryItem and purchaseOrderNumber.
    * @param order An inventory purchase that was the source of the order.
    * @returns A result map containing the totalOrdered, totalReceived, totalRemaining, thisOrderRemaining,
    *                 totalOrderedAmount, totalReceivedAmount, totalRemainingAmount, thisOrderRemainingAmount,
    *                 totalPaymentApproved.
    */
    def calcQuantities(order) {

        def result = [:]

        result.totalOrdered = 0
        result.totalOrderedAmount = 0
        result.totalReceived = 0
        result.totalReceivedAmount = 0
        result.totalPaymentApproved = 0
        InventoryItemPurchase.withCriteria {
            eq("inventoryItem", order.inventoryItem)
            eq("purchaseOrderNumber", order.purchaseOrderNumber)
        }.each() {
            if(it.inventoryItemPurchaseType.id == 1L) { // Orders.
                result.totalOrdered += it.quantity
                result.totalOrderedAmount += it.orderValueAmount
            }
            if(it.inventoryItemPurchaseType.id == 2L || it.inventoryItemPurchaseType.id == 3L) { // Received B/order and Complete.
                result.totalReceived += it.quantity
                result.totalReceivedAmount += it.orderValueAmount
            }
            if(it.inventoryItemPurchaseType.id == 4L) { // Approved.
                result.totalPaymentApproved += it.orderValueAmount
            }
        }

        result.totalRemaining
        if(result.totalOrdered > result.totalReceived)
            result.totalRemaining = result.totalOrdered - result.totalReceived
        else
            result.totalRemaining = 0

        result.totalRemainingAmount
        if(result.totalOrderedAmount > result.totalReceivedAmount)
            result.totalRemainingAmount = result.totalOrderedAmount - result.totalReceivedAmount
        else
            result.totalRemainingAmount = 0

        result.thisOrderRemaining
        if(result.totalRemaining > order.quantity)
            result.thisOrderRemaining = order.quantity
        else
            result.thisOrderRemaining = result.totalRemaining

        result.thisOrderRemainingAmount
        if(result.totalRemainingAmount > order.orderValueAmount)
            result.thisOrderRemainingAmount = order.orderValueAmount
        else
            result.thisOrderRemainingAmount = result.totalRemainingAmount

        return result
    }

    /**
    * Get the original order for an inventoryItemPurchase and InventoryItem.
    * @param inventoryItemPurchase An inventory puchase.
    * @returns The order.
    */
    def getOriginalOrder(inventoryItemPurchase) {

        def namedParams = [:]

        namedParams.inventoryItem = inventoryItemPurchase.inventoryItem
        namedParams.purchaseOrderNumber = inventoryItemPurchase.purchaseOrderNumber
        namedParams.orderPlaced = InventoryItemPurchaseType.read(1)

        def order = InventoryItemPurchase.find("from InventoryItemPurchase as p \
                                                                                where( p.inventoryItem = :inventoryItem \
                                                                                            and p.purchaseOrderNumber = :purchaseOrderNumber \
                                                                                            and p.inventoryItemPurchaseType = :orderPlaced )",
                                                                            namedParams)

        return order
    }

    /**
    * Get costCodes by person and the purchasingGroups they have been assigned.
    * @param person A Person, defaults to currentUser.
    * @returns A list of CostCodes.
    */
    def getCostCodesByPerson(person = authService.currentUser) {
        if(person.purchasingGroups) {
            CostCode.withCriteria {
                    eq('isActive', true)
                    or {
                        person.purchasingGroups.each() { purchasingGroup ->
                            eq('purchasingGroup', purchasingGroup)
                        }
                    }
            }.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } // withCriteria
        }
        else
            []
    } // getCostCodesByPerson

    /**
    * Check if a person is in a purchasing group.
    * @param person A PurchasingGroup to check for.
    * @param person A Person, defaults to currentUser.
    * @returns True if person is in group.
    */
    def isPersonInPurchasingGroup(purchasingGroup, person = authService.currentUser) {
        for(pg in person.purchasingGroups) {
            if(pg.id == purchasingGroup.id)
                return true
        }
    } // isPersonInPurchasingGroup

    def delete(params) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]
            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemPurchase && m.field)
                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
                return result
            }

            result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)

            if(!result.inventoryItemPurchaseInstance)
                return fail(code:"default.not.found")

            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id
            def purchaseTypeId = result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id

            // Handle Invoice Payment Approved.
            if(purchaseTypeId == 4) {
                // Find and mark all orders as invoicePaymentApproved = false.
                InventoryItemPurchase.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
                    inventoryItemPurchaseType {
                            eq("id", 1L) // Order Placed.
                    }
                }.each() {
                    it.invoicePaymentApproved = false
                }
                // Find and mark last orderReceived as invoicePaymentApproved = false.
                InventoryItemPurchase.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
                    inventoryItemPurchaseType {
                        or {
                            eq("id", 2L) // Received B/order To Come
                            eq("id", 3L) // Received Complete
                        }
                    }
                }.last().invoicePaymentApproved = false
            }

            // Handle Received.
            // Refuse to delete if payment approved.
            // Find and reverse the matching movement.
            // Find and mark all orders as receivedComplete = false.
            if(purchaseTypeId == 2 || purchaseTypeId == 3) {

                def paymentAlreadyApproved = InventoryItemPurchase.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
                    inventoryItemPurchaseType {
                            eq("id", 4L) // Invoice Payment Approved.
                    }
                }

                if(paymentAlreadyApproved)
                    return fail(code:"inventoryItemPurchase.delete.failure.payment.approved")

                def startOfDay = dateUtilService.getMidnight(result.inventoryItemPurchaseInstance.date)
                def inventoryMovements = InventoryMovement.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem )
                    eq("quantity", result.inventoryItemPurchaseInstance.quantity)
                    between("date", startOfDay, startOfDay+1)
                    inventoryMovementType {
                        eq("id", 3L) // purchaseReceived.
                    }
                    order('id', 'desc') // The newest one will be first.
                }

                if(inventoryMovements[0]) {
                    def movementResult = inventoryMovementService.reverseMove(inventoryMovements[0])
                    if(movementResult.error)
                        return fail(code:"inventoryItemPurchase.delete.failure.reverse.move")
                }

                InventoryItemPurchase.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
                    inventoryItemPurchaseType {
                            eq("id", 1L) // Order Placed
                    }
                }.each() {
                    it.receivedComplete = false
                }
            } // Handle Received.

            // Handle Order Placed.
            // Refuse to delete if we have received items.
            // Deletion of received already requires payment approved to be deleted.
            if(purchaseTypeId == 1) {

                def calcQuantities = calcQuantities(result.inventoryItemPurchaseInstance)
                if(calcQuantities.totalReceived > 0)
                    return fail(code:"inventoryItemPurchase.delete.failure.received.exists")

                // Cover the case of 0 ordered and received.
                // Only possible to have Received Complete (3L) in this case and not B/order (2L).
                def receivedExists = InventoryItemPurchase.withCriteria {
                    eq("inventoryItem", result.inventoryItemPurchaseInstance.inventoryItem)
                    eq("purchaseOrderNumber", result.inventoryItemPurchaseInstance.purchaseOrderNumber)
                    inventoryItemPurchaseType {
                            eq("id", 3L) // Received Complete.
                    }
                }

                if(receivedExists)
                    return fail(code:"inventoryItemPurchase.delete.failure.received.exists")
            }

            // Success.
            // By this stage everything should be handled and the delete call is allowed and expected to pass.
            result.inventoryItemPurchaseInstance.delete()
            return result

        } // end withTransaction
    }

    def edit(params) {
        def result = [:]
        def fail = { Map m ->
            result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
            return result
        }

        result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)

        if(!result.inventoryItemPurchaseInstance)
            return fail(code:"default.not.found")

        // Success.
        return result
    }

    def update(params) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemPurchaseInstance && m.field)
                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
                return result
            }

            result.inventoryItemPurchaseInstance = InventoryItemPurchase.get(params.id)

            if(!result.inventoryItemPurchaseInstance)
                return fail(code:"default.not.found")

            // Optimistic locking check.
            if(params.version) {
                if(result.inventoryItemPurchaseInstance.version > params.version.toLong())
                    return fail(field:"version", code:"default.optimistic.locking.failure")
            }

            def originalPaymentApprovedAmount = result.inventoryItemPurchaseInstance.orderValueAmount

            result.inventoryItemPurchaseInstance.properties = params
            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
            result.inventoryItemPurchaseInstance.invoiceNumber = result.inventoryItemPurchaseInstance.invoiceNumber.trim()
            result.inventoryItemPurchaseInstance.lastUpdatedBy = authService.currentUser

            // Fetch to prevent lazy initialization error.
            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure

            //  If processing an Invoice Approved.
            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {
                if(!result.inventoryItemPurchaseInstance.invoiceNumber)
                    return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")
            }

            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
                return fail(code:"default.update.failure")

            //  If processing an Invoice Approved.
            if(result.inventoryItemPurchaseInstance.inventoryItemPurchaseType.id == 4L) {

                def order = getOriginalOrder(result.inventoryItemPurchaseInstance)
                if(!order)
                    return fail(code:"default.not.found")

                // Update orderValueAmount if receivedComplete.
                if(order.receivedComplete) {

                    def calcQuantities = calcQuantities(order)
                    order.orderValueAmount = calcQuantities.totalPaymentApproved

                    if(order.hasErrors() || !order.save())
                        return fail(code:"default.create.failure")
                }

            }

            // Success.
            return result

        } //end withTransaction
    }  // end update()

    def save(params) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemPurchase && m.field)
                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
                return result
            }

            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
            result.inventoryItemPurchaseInstance.purchaseOrderNumber = result.inventoryItemPurchaseInstance.purchaseOrderNumber.trim()
            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(1) // Order

            // Fetch to prevent lazy initialization error.
            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure

            // Prevent ordering on obsolete or inactive inventoryItem.
            def isObsolete = result.inventoryItemPurchaseInstance.inventoryItem?.isObsolete
            def isActive = result.inventoryItemPurchaseInstance.inventoryItem?.isActive
            if(isObsolete || !isActive)
                return fail(code:"inventoryItemPurchase.operation.not.permitted.on.inactive.or.obsolete.item")

            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
                return fail(code:"default.create.failure")

            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id

            // success
            return result

        } // end withTransaction
    } // save()

    def receiveSave(params) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemPurchase && m.field)
                    result.inventoryItemPurchase.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
                return result
            }

            def order = InventoryItemPurchase.get(params.orderId)
            if(!order)
                return fail(code:"default.not.found")
            result.orderId = order.id

            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
            result.inventoryItemPurchaseInstance.costCode = order.costCode
            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency

            def calcQuantities = calcQuantities(order)
            if(result.inventoryItemPurchaseInstance.quantity)
                calcQuantities.totalReceived += result.inventoryItemPurchaseInstance.quantity
            if(result.inventoryItemPurchaseInstance.orderValueAmount)
                calcQuantities.totalReceivedAmount += result.inventoryItemPurchaseInstance.orderValueAmount

            if(calcQuantities.totalReceived >= calcQuantities.totalOrdered) {
                order.receivedComplete = true
                result.inventoryItemPurchaseInstance.receivedComplete = true
                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(3) // Received Complete.
            }
            else {
                order.receivedComplete = false
                result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(2) // Received B/oder to Come.
            }

            // Fetch to prevent lazy initialization error.
            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure
            result.inventoryItemPurchaseInstance.inventoryItem.inventoryLocation

            if(order.hasErrors() || !order.save())
                return fail(code:"default.create.failure")

            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
                return fail(code:"default.create.failure")

            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id

            // Perform the inventory movement.
            if(result.inventoryItemPurchaseInstance.quantity > 0) {
                def p = [:]
                p.inventoryItem = result.inventoryItemPurchaseInstance.inventoryItem
                p.quantity = result.inventoryItemPurchaseInstance.quantity
                p.inventoryMovementType = InventoryMovementType.read(3)
                def movementResult = inventoryMovementService.move(p)
                if(movementResult.error)
                    return fail(code:"default.create.failure")
            }

            // success
            return result

        } // end withTransaction
    } // receiveSave()

    def approveInvoicePaymentSave(params) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemPurchaseInstance && m.field)
                    result.inventoryItemPurchaseInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItemPurchase", params.id] ]
                return result
            }

            def received = InventoryItemPurchase.get(params.receivedId)
            if(!received)
                return fail(code:"default.not.found")
            result.receivedId = received.id

            def order = getOriginalOrder(received)
            if(!order)
                return fail(code:"default.not.found")
            result.orderId = order.id

            result.inventoryItemPurchaseInstance = new InventoryItemPurchase(params)
            result.inventoryItemPurchaseInstance.enteredBy = authService.currentUser
            result.inventoryItemPurchaseInstance.purchaseOrderNumber = order.purchaseOrderNumber
            result.inventoryItemPurchaseInstance.costCode = order.costCode
            result.inventoryItemPurchaseInstance.orderValueCurrency = order.orderValueCurrency
            result.inventoryItemPurchaseInstance.inventoryItemPurchaseType = InventoryItemPurchaseType.read(4) // Approve.

            received.invoicePaymentApproved = true
            result.inventoryItemPurchaseInstance.invoicePaymentApproved = true

            // Update orderValueAmount and invoicePaymentApproved if processing a receivedComplete.
            if(received.inventoryItemPurchaseType.id == 3L) {
                order.invoicePaymentApproved = true
                result.inventoryItemPurchaseInstance.receivedComplete = true
                def calcQuantities = calcQuantities(order)
                if(result.inventoryItemPurchaseInstance.orderValueAmount)
                    calcQuantities.totalPaymentApproved += result.inventoryItemPurchaseInstance.orderValueAmount
                order.orderValueAmount = calcQuantities.totalPaymentApproved
            }

            // Fetch to prevent lazy initialization error.
            result.inventoryItemPurchaseInstance.inventoryItem.unitOfMeasure

            if(!result.inventoryItemPurchaseInstance.invoiceNumber)
                return fail(field:"invoiceNumber", code:"inventoryItemPurchase.invoiceNumber.required")

            if(order.hasErrors() || !order.save())
                return fail(code:"default.create.failure")

            if(result.inventoryItemPurchaseInstance.hasErrors() || !result.inventoryItemPurchaseInstance.save())
                return fail(code:"default.create.failure")

            result.inventoryItemId = result.inventoryItemPurchaseInstance.inventoryItem.id

            // Success..
            return result

        } // end withTransaction
    } // approveInvoicePaymentSave()

} // end class
