/**
 * Copyright (c) 2012 Partners In Health.  All rights reserved.
 * The use and distribution terms for this software are covered by the
 * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
 * which can be found in the file epl-v10.html at the root of this distribution.
 * By using this software in any fashion, you are agreeing to be bound by
 * the terms of this license.
 * You must not remove this notice, or any other, from this software.
 **/
package org.pih.warehouse.inventory

import grails.converters.JSON
import grails.validation.ValidationException
import org.grails.plugins.csv.CSVWriter
import groovy.time.TimeCategory
import org.pih.warehouse.core.Location
import org.pih.warehouse.core.Person
import org.pih.warehouse.core.User
import org.pih.warehouse.order.OrderItem
import org.pih.warehouse.product.Product
import org.pih.warehouse.product.ProductException
import org.pih.warehouse.product.ProductPackage
import org.pih.warehouse.requisition.RequisitionItem
import org.pih.warehouse.shipping.Container
import org.pih.warehouse.shipping.Shipment
import org.pih.warehouse.shipping.ShipmentItem
import org.pih.warehouse.shipping.ShipmentItemException
import org.pih.warehouse.util.DateUtil

import java.text.DateFormat
import java.text.SimpleDateFormat

class InventoryItemController {

    def dataService
    def inventoryService
    def shipmentService
    def requisitionService
    def orderService
    def forecastingService
    def userService
    def productAvailabilityService
    def grailsApplication


    def index = {
        redirect(controller: "inventory", action: "browse")
    }

    /**
     * Ajax method for the Record Inventory page.
     */
    def getInventoryItems = {
        log.info params
        def productInstance = Product.get(params?.product?.id)
        def inventoryItemList = inventoryService.getInventoryItemsByProduct(productInstance)
        render inventoryItemList as JSON
    }


    /**
     * Displays the stock card for a product
     */
    def showStockCard = { StockCardCommand cmd ->

        try {
            // add the current warehouse to the command object which prevents location from being spoofed
            cmd.warehouse = Location.get(session?.warehouse?.id)

            // now populate the rest of the commmand object
            inventoryService.getStockCardCommand(cmd, params)
            [commandInstance: cmd]
        } catch (ProductException e) {
            flash.message = e.message
            redirect(controller: "dashboard", action: "index")
        }
    }

    def showCurrentStock = { StockCardCommand cmd ->
        def startTime = System.currentTimeMillis()
        cmd.warehouse = Location.get(session?.warehouse?.id)
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)
        commandInstance.product = Product.get(params.id)
        log.info "${controllerName}.${actionName}: " + (System.currentTimeMillis() - startTime) + " ms"
        log.info "Product " + commandInstance.product
        render(template: "showCurrentStock", model: [commandInstance: commandInstance])
    }


    def showCurrentStockAllLocations = { StockCardCommand cmd ->
        def startTime = System.currentTimeMillis()
        User currentUser = User.get(session.user.id)
        Location currentLocation = Location.get(session?.warehouse?.id)
        cmd.warehouse = currentLocation
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)
        def quantityMap = inventoryService.getCurrentStockAllLocations(commandInstance?.product, currentLocation, currentUser)
        if (params.download && quantityMap) {
            def sw = new StringWriter()

            def csv = new CSVWriter(sw, {
                "Code" { it.code }
                "Product" { it.product }
                "Location Group" { it.locationGroup }
                "Location" { it.location }
                "Quantity on Hand" { it.qtyOnHand }
                "Total Value" { it.totalValue }
            })

            quantityMap.each {
                it.each { key, values ->
                    values.locations.each { value ->
                        csv << [
                                code         : commandInstance?.product?.productCode,
                                product      : commandInstance?.product?.name,
                                locationGroup: key ?: g.message(code: 'locationGroup.empty.label'),
                                location     : value?.location?.name,
                                qtyOnHand    : value.quantity,
                                totalValue   : value.value,
                        ]
                    }
                }
            }

            response.setHeader("Content-disposition", "attachment; filename=\"All-Locations-${commandInstance?.product?.productCode}.csv\"")
            render(contentType: "text/csv", text: sw.toString(), encoding: "UTF-8")
            return
        } else {
            def targetUri = "/inventoryItem/showStockCard/${commandInstance?.product?.id}"
            log.info "${controllerName}.${actionName}: " + (System.currentTimeMillis() - startTime) + " ms"

            render(template: "showCurrentStockAllLocations", model: [commandInstance: commandInstance, quantityMap: quantityMap, targetUri: targetUri])
        }
    }

    def showAlternativeProducts = { StockCardCommand cmd ->
        def startTime = System.currentTimeMillis()
        def product = Product.get(params.id)
        def location = Location.get(session?.warehouse?.id)

        def products = product.alternativeProducts() as List
        log.info "Products " + products
        def quantityMap = [:]
        if (!products.isEmpty()) {
            quantityMap = inventoryService.getQuantityByProductMap(location, products)
        }
        def totalQuantity = quantityMap.values().sum() ?: 0

        log.info "${controllerName}.${actionName}: " + (System.currentTimeMillis() - startTime) + " ms"

        render(template: "showProductGroups", model: [product: product, totalQuantity: totalQuantity, quantityMap: quantityMap])
    }


    def showStockHistory = { StockCardCommand cmd ->
        def startTime = System.currentTimeMillis()
        // add the current warehouse to the command object
        cmd.warehouse = Location.get(session?.warehouse?.id)

        // now populate the rest of the commmand object
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)

        def stockHistoryList = []


        int totalDebit = 0, totalCredit = 0, totalBalance = 0, totalCount = 0
        def balance = [:]
        def count = [:]
        def transactionMap = commandInstance?.getTransactionLogMap(false)
        def previousTransaction = null


        transactionMap.each { Transaction transaction, List transactionEntries ->

            // For PRODUCT INVENTORY transactions we just need to clear the balance completely and start over
            if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.PRODUCT_INVENTORY) {
                balance = [:]
                count = [:]
                totalCredit = 0
                totalDebit = 0
            }

            transactionEntries.eachWithIndex { TransactionEntry transactionEntry, i ->

                boolean isBaseline = false
                String index = (transactionEntry.binLocation?.name ?: "DefaultBin") + "-" + (transactionEntry?.inventoryItem?.lotNumber ?: "DefaultLot")

                if (!balance[index]) {
                    balance[index] = 0
                    count[index] = 0
                }

                if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.DEBIT) {
                    balance[index] -= transactionEntry?.quantity
                    totalDebit += transactionEntry?.quantity
                } else if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.CREDIT) {
                    balance[index] += transactionEntry?.quantity
                    totalCredit += transactionEntry?.quantity
                } else if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.INVENTORY) {
                    balance[index] = transactionEntry?.quantity
                    count[index] = transactionEntry?.quantity
                } else if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.PRODUCT_INVENTORY) {
                    balance[index] += transactionEntry?.quantity
                    count[index] += transactionEntry?.quantity
                }

                if (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.PRODUCT_INVENTORY && i == 0) {
                    isBaseline = true
                }

                boolean isCredit = (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.CREDIT && transactionEntry?.quantity >= 0) ||
                        (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.DEBIT && transactionEntry.quantity < 0)

                boolean isDebit = (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.DEBIT && transactionEntry?.quantity > 0) ||
                        (transaction?.transactionType?.transactionCode == org.pih.warehouse.inventory.TransactionCode.CREDIT && transactionEntry.quantity < 0)

                // Normalize quantity (inventory transactions were all converted to CREDIT so some may have negative quantity)
                def quantity = (transactionEntry.quantity > 0) ? transactionEntry.quantity : -transactionEntry.quantity

                String transactionYear = (transaction.transactionDate.year + 1900).toString()
                String transactionMonth = (transaction.transactionDate.month).toString()

                stockHistoryList << [
                        transactionYear  : transactionYear,
                        transactionMonth : transactionMonth,
                        transactionDate  : transaction.transactionDate,
                        transactionCode  : transaction?.transactionType?.transactionCode,
                        transaction      : transaction,
                        shipment         : null,
                        requisition      : null,
                        binLocation      : transactionEntry.binLocation,
                        inventoryItem    : transactionEntry.inventoryItem,
                        comments         : transactionEntry.comments,
                        quantity         : quantity,
                        isDebit          : isDebit,
                        isCredit         : isCredit,
                        balance          : balance.values().sum(),
                        showDetails      : (i == 0),
                        isBaseline       : isBaseline,
                        isSameTransaction: (previousTransaction?.id == transaction?.id),
                ]
                previousTransaction = transaction
            }

            totalBalance = balance.values().sum()
            totalCount = count.values().sum()
        }


        log.info "${controllerName}.${actionName}: " + (System.currentTimeMillis() - startTime) + " ms"

        if (params.print) {
            render(template: "printStockHistory", model: [commandInstance: commandInstance, stockHistoryList: stockHistoryList,
                                                          totalBalance   : totalBalance, totalCount: totalCount, totalCredit: totalCredit, totalDebit: totalDebit])
        } else {
            stockHistoryList = stockHistoryList.groupBy({ it.transactionYear })
            def groupedStockHistoryList = [:]
            stockHistoryList.each { year, history ->
                history = history.groupBy { it.transactionMonth }
                groupedStockHistoryList.get(year, [:]) << history
            }
            render(template: "showStockHistory", model: [commandInstance: commandInstance, stockHistoryList: groupedStockHistoryList,
                                                          totalBalance   : totalBalance, totalCount: totalCount, totalCredit: totalCredit, totalDebit: totalDebit])
        }
    }

    def showSuppliers = {

        def productInstance = Product.get(params.id)
        Location currentLocation = Location.get(session?.warehouse?.id)

        render(template: "showSuppliers", model: [productInstance: productInstance, currentLocation: currentLocation])
    }


    def showPendingInbound = {

        Product product = Product.get(params.id)
        Location location = Location.get(session?.warehouse?.id)
        def itemsMap = [:]
        def shipmentItems, orderItems = []

        shipmentItems = shipmentService.getPendingInboundShipmentItems(location, product)
        shipmentItems.sort { it.shipment.currentStatus }.groupBy { it.shipment }.collect { k, v ->
            itemsMap.put(k, [
                    quantityRemaining: v.quantityRemaining.sum(),
                    quantityPurchased: 0,
                    shipDate: k.expectedShippingDate,
                    type: 'Stock Movement'
            ]
            )
        }
        orderItems = orderService.getPendingInboundOrderItems(location, product)
        orderItems.collect {
            def existingItem = itemsMap.find {k, v -> k instanceof OrderItem && k.actualReadyDate == it.actualReadyDate && k.order == it.order}
            if (!existingItem) {
                itemsMap.put(it, [
                        quantityRemaining: 0,
                        quantityPurchased: (it.quantityRemaining * it.quantityPerUom).toInteger(),
                        shipDate         : it.actualReadyDate,
                        type             : 'Purchase Order',
                ]
                )
            } else {
                itemsMap[existingItem.getKey()].quantityPurchased += (it.quantityRemaining * it.quantityPerUom).toInteger()
            }
        }


        log.info "itemsMap: " + itemsMap

        render(template: "showPendingInboundStock", model: [product: product, itemsMap: itemsMap])
    }

    def showPendingOutbound = {
        Product product = Product.get(params.id)
        Location location = Location.get(session?.warehouse?.id)
        def itemsMap = [:]
        def requisitionItems

        requisitionItems = requisitionService.getPendingRequisitionItems(location, product)
        requisitionItems.groupBy { it.requisition }.collect { k, v ->
            itemsMap.put(k, [
                    quantityRequested: v.quantity.sum(),
                    quantityRequired: v.sum() { RequisitionItem requisitionItem -> requisitionItem.calculateQuantityRequired() },
                    quantityPicked: v.sum() { RequisitionItem requisitionItem -> requisitionItem.calculateQuantityPicked() },
            ]
            )
        }


        log.info "itemsMap: " + itemsMap

        render(template: "showPendingOutboundStock", model: [product: product, itemsMap: itemsMap])
    }

    def showDemand = { StockCardCommand cmd ->
        use(TimeCategory) {
            DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy")
            // By default set last 12 months
            Integer demandPeriod = grailsApplication.config.openboxes.forecasting.demandPeriod?:365
            Map defaultDateRange = DateUtil.getDateRange(new Date(), -1)
            params.startDate = params.startDate ? dateFormat.parse(params.startDate) : defaultDateRange.endDate - demandPeriod.days
            params.endDate = params.endDate ? dateFormat.parse(params.endDate) : defaultDateRange.endDate
        }

        // add the current warehouse to the command object
        cmd.warehouse = Location.get(session?.warehouse?.id)
        Location destination = params.destination ? Location.get(params.destination.id) : null

        // now populate the rest of the commmand object
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)

        DateFormat monthFormat = new SimpleDateFormat("MMM yyyy")
        monthFormat.timeZone = TimeZone.default

        def requisitionItemsDemandDetails = forecastingService.getDemandDetailsForDemandTab(
            cmd.warehouse,
            destination,
            commandInstance?.product,
            params.startDate,
            params.endDate
        )

        def destinations = forecastingService.getAvailableDestinationsForDemandDetails(
                cmd.warehouse,
                commandInstance?.product,
                params.startDate,
                params.endDate
        )

        requisitionItemsDemandDetails = requisitionItemsDemandDetails.collect {
            [
                    status           : it?.request_status,
                    productCode      : it?.product_code,
                    productName      : it?.product_name,
                    origin           : it?.origin_name,
                    requisitionId    : it?.request_id,
                    requestNumber    : it?.request_number,
                    destination      : it?.destination_name,
                    dateIssued       : it?.date_issued,
                    monthIssued      : it?.date_issued ? monthFormat.format(it?.date_issued) : null,
                    dateRequested    : it?.date_requested,
                    monthRequested   : monthFormat.format(it?.date_requested),
                    quantityRequested: it?.quantity_requested ?: 0,
                    quantityIssued   : it?.quantity_picked ?: 0,
                    quantityDemand   : it?.quantity_demand ?: 0,
                    reasonCode       : it?.reason_code_classification,
            ]
        }

        // Create list of months to display including all months between first requested date and selected end date
        def monthKeys = (params.startDate..params.endDate).collect {
            monthFormat.format(it)
        }.unique()

        render(template: "showDemand",
                model: [commandInstance : commandInstance,
                        requisitionItems: requisitionItemsDemandDetails,
                        numberOfMonths  : monthKeys?.size() ?: 1,
                        monthKeys       : monthKeys,
                        params          : params,
                        destinations    : destinations])
    }

    def showProductDemand = {
        Product product = Product.get(params.id)
        Location location = Location.get(session.warehouse.id)
        if (params.format == 'csv') {
            def data = forecastingService.getDemandDetails(location, product)
            def csv = dataService.generateCsv(data)
            def filename = "Product Demand ${product.productCode} ${location.name}.csv"
            response.setHeader("Content-disposition", "attachment; filename=\"${filename}\"")
            render(contentType: "text/csv", text: csv)

            return
        }

        render(template: "showProductDemand", model: [product: product])
    }


    def showInventorySnapshot = {
        def location = Location.get(session.warehouse.id)
        def product = Product.get(params.id)
        def inventorySnapshots = InventorySnapshot.findAllByProductAndLocation(product, location)
        render(template: "showInventorySnapshot", model: [inventorySnapshots: inventorySnapshots, product: product])

    }


    def showDocuments = {
        def productInstance = Product.get(params.id)
        render(template: "showDocuments", model: [productInstance: productInstance])
    }


    /**
     * Displays the stock card for a product
     */
    def showLotNumbers = { StockCardCommand cmd ->
        // add the current warehouse to the command object
        cmd.warehouse = Location.get(session?.warehouse?.id)

        // now populate the rest of the commmand object
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)


        [commandInstance: commandInstance]
    }

    /**
     * Displays the stock card for a product
     */
    def showTransactionLog = { StockCardCommand cmd ->
        // add the current warehouse to the command object
        cmd.warehouse = Location.get(session?.warehouse?.id)

        // now populate the rest of the commmand object
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)

        [commandInstance: commandInstance]
    }


    /**
     * Displays the stock card for a product
     */
    def showGraph = { StockCardCommand cmd ->
        // add the current warehouse to the command object
        cmd.warehouse = Location.get(session?.warehouse?.id)

        // now populate the rest of the commmand object
        def commandInstance = inventoryService.getStockCardCommand(cmd, params)

        log.info("Inventory item list: " + commandInstance?.inventoryItemList)
        [commandInstance: commandInstance]
    }

    /**
     * Display the Record Inventory form for the product
     */
    def showRecordInventory = { RecordInventoryCommand commandInstance ->

        def locationInstance = Location.get(session?.warehouse?.id)

        // We need to set the inventory instance in order to save an 'inventory' transaction
        if (!commandInstance.inventory) {
            commandInstance.inventory = locationInstance?.inventory
        }
        inventoryService.populateRecordInventoryCommand(commandInstance, params)

        Product productInstance = commandInstance.product
        List transactionEntryList = inventoryService.getTransactionEntriesByInventoryAndProduct(commandInstance?.inventory, [productInstance])

        // Get the inventory warning level for the given product and inventory
        commandInstance.inventoryLevel = InventoryLevel.findByProductAndInventory(productInstance, commandInstance?.inventory)

        // Compute the total quantity for the given product
        commandInstance.totalQuantity = inventoryService.getQuantityByProductMap(transactionEntryList)[productInstance] ?: 0

        commandInstance.totalQuantityAvailableToPromise = inventoryService.getQuantityAvailableToPromise(commandInstance.product, commandInstance?.inventory?.warehouse)

        // FIXME Use this method instead of getQuantityByProductMap
        // NEED to add tests before we introduce this change
        //commandInstance.totalQuantity = inventoryService.getQuantityOnHand(locationInstance, productInstance)

        Map<Product, List<InventoryItem>> inventoryItems = inventoryService.getInventoryItemsWithQuantity([productInstance], commandInstance.inventory)
        def result = []
        inventoryItems.keySet().each { product ->
            result = inventoryItems[product].collect { ((InventoryItem) it).toJson() }
        }
        String jsonString = [product: productInstance.toJson(), inventoryItems: result] as JSON
        log.info "record inventory " + jsonString

        [commandInstance: commandInstance, product: jsonString]
    }

    def saveRecordInventory = { RecordInventoryCommand commandInstance ->
        log.info("Before saving record inventory " + params)
        inventoryService.saveRecordInventoryCommand(commandInstance, params)
        if (!commandInstance.hasErrors()) {
            redirect(action: "showStockCard", params: ['product.id': commandInstance.product.id])
            return
        }

        log.info("User chose to validate or there are errors")
        def warehouseInstance = Location.get(session?.warehouse?.id)

        commandInstance.inventory = warehouseInstance?.inventory
        commandInstance.inventoryLevel = InventoryLevel.findByProductAndInventory(commandInstance?.product, commandInstance?.inventory)

        Product productInstance = commandInstance.product
        List transactionEntryList = inventoryService.getTransactionEntriesByInventoryAndProduct(commandInstance?.inventory, [productInstance])

        // Get the inventory warning level for the given product and inventory
        commandInstance.inventoryLevel = InventoryLevel.findByProductAndInventory(commandInstance?.product, commandInstance?.inventory)

        commandInstance.totalQuantity = inventoryService.getQuantityByProductMap(transactionEntryList)[productInstance] ?: 0

        log.info "commandInstance.recordInventoryRows: "
        commandInstance?.recordInventoryRows.each {
            log.info "it ${it?.id}:${it?.lotNumber}:${it?.oldQuantity}:${it?.newQuantity}"
        }

        render(view: "showRecordInventory", model: [commandInstance: commandInstance])
    }

    def showTransactions = {

        def warehouseInstance = Location.get(session?.warehouse?.id)
        def productInstance = Product.get(params?.product?.id)
        def inventoryInstance = warehouseInstance.inventory
        def inventoryItemList = inventoryService.getInventoryItemsByProductAndInventory(productInstance, inventoryInstance)
        def transactionEntryList = TransactionEntry.findAllByProductAndInventory(productInstance, inventoryInstance)
        def inventoryLevelInstance = InventoryLevel.findByProductAndInventory(productInstance, inventoryInstance)

        [inventoryInstance     : inventoryInstance,
         inventoryLevelInstance: inventoryLevelInstance,
         productInstance       : productInstance,
         inventoryItemList     : inventoryItemList,
         transactionEntryList  : transactionEntryList,
         transactionEntryMap   : transactionEntryList.groupBy { it.transaction }]
    }


    def createInventoryItem = {

        flash.message = "${warehouse.message(code: 'inventoryItem.temporaryCreateInventoryItem.message')}"

        def productInstance = Product.get(params?.product?.id)
        def inventoryInstance = Inventory.get(params?.inventory?.id)
        def itemInstance = new InventoryItem(product: productInstance)
        def inventoryLevelInstance = inventoryService.getInventoryLevelByProductAndInventory(productInstance, inventoryInstance)
        def inventoryItems = inventoryService.getInventoryItemsByProduct(productInstance)
        [itemInstance: itemInstance, inventoryInstance: inventoryInstance, inventoryItems: inventoryItems, inventoryLevelInstance: inventoryLevelInstance, totalQuantity: totalQuantity]
    }

    def saveInventoryItem = {
        log.info "save inventory item " + params
        def productInstance = Product.get(params.product.id)
        def inventoryInstance = Inventory.get(params.inventory.id)
        def inventoryItem = new InventoryItem(params)
        def inventoryItems = inventoryService.getInventoryItemsByProduct(inventoryItem.product)
        inventoryInstance.properties = params

        def transactionInstance = new Transaction(params)
        def transactionEntry = new TransactionEntry(params)
        if (!transactionEntry.quantity) {
            transactionEntry.errors.rejectValue("quantity", 'transactionEntry.quantity.invalid')
        }

        if (transactionEntry.hasErrors()) {
            inventoryItem.errors = transactionEntry.errors
        }
        if (transactionInstance.hasErrors()) {
            inventoryItem.errors = transactionInstance.errors
        }


        // TODO Move all of this logic into the service layer in order to take advantage of Hibernate/Spring transactions
        if (!inventoryItem.hasErrors() && inventoryItem.save()) {
            // Need to create a transaction if we want the inventory item
            // to show up in the stock card
            transactionInstance.transactionDate = new Date()
            transactionInstance.transactionType = TransactionType.get(Constants.INVENTORY_TRANSACTION_TYPE_ID)
            def warehouseInstance = Location.get(session.warehouse.id)
            transactionInstance.source = warehouseInstance
            transactionInstance.inventory = warehouseInstance.inventory

            transactionEntry.inventoryItem = inventoryItem
            transactionInstance.addToTransactionEntries(transactionEntry)

            transactionInstance.save()
            flash.message = "${warehouse.message(code: 'inventoryItem.savedItemWithinNewTransaction.message', args: [inventoryItem.id, transactionInstance.id])}"

        } else {
            render(view: "createInventoryItem", model: [itemInstance: inventoryItem, inventoryInstance: inventoryInstance, inventoryItems: inventoryItems])
            return
        }


        // If all else fails, return to the show stock card page
        redirect(action: 'showStockCard', id: productInstance?.id)
    }


    def edit = {
        def itemInstance = InventoryItem.get(params.id)
        if (!itemInstance) {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), params.id])}"
            redirect(action: "show", id: itemInstance.id)
        } else {
            return [itemInstance: itemInstance]
        }
    }

    def editInventoryLevel = {

        def productInstance = Product.get(params?.product?.id)
        def inventoryInstance = Inventory.get(params?.inventory?.id)
        if (!inventoryInstance) {
            def warehouse = Location.get(session?.warehouse?.id)
            inventoryInstance = warehouse.inventory
        }

        def inventoryLevelInstance = InventoryLevel.findByProductAndInventory(productInstance, inventoryInstance)
        if (!inventoryLevelInstance) {
            inventoryLevelInstance = new InventoryLevel()
        }

        [productInstance: productInstance, inventoryInstance: inventoryInstance, inventoryLevelInstance: inventoryLevelInstance]
    }

    def updateInventoryLevel = {

        log.info("update inventory level " + params)

        def productInstance = Product.get(params?.product?.id)
        def inventoryInstance = Inventory.get(params?.inventory?.id)
        def inventoryLevelInstance = InventoryLevel.get(params.id)

        if (inventoryLevelInstance) {
            inventoryLevelInstance.properties = params
        } else {
            inventoryLevelInstance = new InventoryLevel(params)
        }

        if (!inventoryLevelInstance.hasErrors() && inventoryLevelInstance.save()) {
            log.info("save inventory level ")
            flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'inventoryLevel.label', default: 'Inventory level')])}"
        } else {
            log.info("render with errors")
            render(view: "updateInventoryLevel", model:
                    [productInstance: productInstance, inventoryInstance: inventoryInstance, inventoryLevelInstance: inventoryLevelInstance])
            return
        }

        redirect(controller: "inventoryItem", action: "showStockCard", id: productInstance?.id)
    }


    /**
     * Handles form submission from Show Stock Card > Adjust Stock dialog.
     */
    def adjustStock = { AdjustStockCommand command ->
        InventoryItem inventoryItem = command.inventoryItem
        try {
            inventoryService.adjustStock(command)
            if (!command.hasErrors()) {
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), inventoryItem.id])}"
            }
        } catch (ValidationException e) {
            command.errors = e.errors
        }

        if (params.redirectUri) {
            redirect(uri: params.redirectUri)
            return
        }


        chain(controller: "inventoryItem", action: "showStockCard",
                id: inventoryItem?.product?.id, params: ['inventoryItem.id': inventoryItem?.id], model: [command: command])
    }

    def showDialog = {
        def location = Location.get(session.warehouse.id)
        def inventoryItem = InventoryItem.get(params.id)
        def binLocation = Location.get(params.binLocation)
        def quantityAvailable = inventoryService.getQuantityFromBinLocation(location, binLocation, inventoryItem)
        def existsInOtherLocation = inventoryService.isInventoryItemInOtherLocation(location.inventory, inventoryItem)

        render(template: params.template, model: [location         : location,
                                                  binLocation      : binLocation,
                                                  inventoryItem    : inventoryItem,
                                                  quantityAvailable: quantityAvailable,
                                                  existsInOtherLocation: existsInOtherLocation])
    }

    def refreshBinLocation = {
        log.info "params: " + params
        render g.selectBinLocationByLocation(params)
    }


    def transferStock = { TransferStockCommand command ->
        log.info "Transfer stock " + params
        log.info "Command " + command

        InventoryItem inventoryItem = command.inventoryItem

        if (inventoryItem) {

            Transaction transaction
            try {
                transaction = inventoryService.transferStock(command)

                if (!transaction.hasErrors()) {
                    flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), inventoryItem.id])}"
                } else {
                    chain(controller: "inventoryItem", action: "showStockCard", id: inventoryItem?.product?.id, model: [transaction: transaction])
                    return
                }

            } catch (Exception e) {
                log.error("Error transferring stock " + e.message, e)
                flash.transaction = transaction
                chain(controller: "inventoryItem", action: "showStockCard", id: inventoryItem?.product?.id, model: [transaction: transaction, itemInstance: inventoryItem])
                return
            }
            log.info("transaction " + transaction + " " + transaction?.id)


        }
        redirect(controller: "inventoryItem", action: "showStockCard", id: inventoryItem?.product?.id, params: ['inventoryItem.id': inventoryItem?.id])
    }


    def update = {

        log.info "Params " + params
        def itemInstance = InventoryItem.get(params.id)
        def productInstance = Product.get(params?.product?.id)
        def date = grailsApplication.config.openboxes.expirationDate.minValue
        if (itemInstance) {
            if (params.version) {
                def version = params.version.toLong()
                if (itemInstance.version > version) {
                    itemInstance.errors.rejectValue("version", "default.optimistic.locking.failure", [warehouse.message(code: 'inventoryItem.label', default: 'Inventory Item')] as Object[], "Another user has updated this inventory item while you were editing")
                    //render(view: "show", model: [itemInstance: itemInstance])
                    redirect(controller: "inventoryItem", action: "showStockCard", id: productInstance?.id)
                    return
                }
            }

            if(itemInstance.product && itemInstance.product.lotAndExpiryControl && (!params.expirationDate || !params.lotNumber)) {
                flash.error = "${warehouse.message(code: 'inventoryItem.lotAndExpiryControl.message')}"
                redirect(controller: "inventoryItem", action: "showStockCard", id: productInstance?.id)
                return
            }

            itemInstance.properties = params

            // FIXME Temporary hack to handle a changed values for these two fields
            itemInstance.lotNumber = params?.lotNumber

            if (!itemInstance.hasErrors() && itemInstance.save(flush: true)) {
                flash.message = "${warehouse.message(code: 'default.updated.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), itemInstance.id])}"
            } else {
                flash.error = "${warehouse.message(code: 'default.not.updated.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), itemInstance.id])}"
                log.info "There were errors trying to save inventory item " + itemInstance?.errors
                if (params.expirationDate < date) {
                    flash.error = "This date is invalid. Please enter a date after ${date.getYear()+1900}."
                    redirect(controller: "inventoryItem", action: "showLotNumbers", id: productInstance?.id)
                    return
                }
            }
        } else {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), params.id])}"
        }
        redirect(controller: "inventoryItem", action: "showStockCard", id: productInstance?.id)
    }


    def deleteTransactionEntry = {
        def transactionEntry = TransactionEntry.get(params.id)
        def productInstance
        if (transactionEntry) {
            productInstance = transactionEntry.inventoryItem.product
            transactionEntry.delete()
        }
        redirect(action: 'showStockCard', params: ['product.id': productInstance?.id])
    }

    def addToInventory = {
        def product = Product.get(params.id)
        render warehouse.message(code: 'inventoryItem.productAddedToInventory.message', args: [product.name])
        //return product as XML
    }
    /**
     * Add a shipment item to a shipment
     */
    def addToShipment = {
        log.info "params" + params
        def shipmentInstance = null
        def containerInstance = null
        def productInstance = Product.get(params?.product?.id)
        def personInstance = Person.get(params?.recipient?.id)
        def binLocation = Location.get(params?.binLocation?.id)
        def inventoryItem = InventoryItem.get(params?.inventoryItem?.id)

        def shipmentContainer = params.shipmentContainer?.split(":")

        shipmentInstance = Shipment.get(shipmentContainer[0])
        containerInstance = Container.get(shipmentContainer[1])

        log.info("shipment " + shipmentInstance)
        log.info("container " + containerInstance)


        def shipmentItem = new ShipmentItem(
                product: productInstance,
                binLocation: binLocation,
                lotNumber: inventoryItem.lotNumber ?: '',
                expirationDate: inventoryItem?.expirationDate,
                inventoryItem: inventoryItem,
                quantity: params.quantity,
                recipient: personInstance,
                shipment: shipmentInstance,
                container: containerInstance)

        try {

            shipmentService.validateShipmentItem(shipmentItem)

            if (shipmentItem.hasErrors() || !shipmentItem.validate()) {
                flash.message = "${warehouse.message(code: 'inventoryItem.errorValidatingItem.message')}\n"
                shipmentItem.errors.each { flash.message += it }

            }

            if (!shipmentItem.hasErrors()) {
                if (!shipmentInstance.addToShipmentItems(shipmentItem).save()) {
                    log.error("Sorry, unable to add new item to shipment.  Please try again.")
                    flash.message = "${warehouse.message(code: 'inventoryItem.unableToAddItemToShipment.message')}"
                } else {
                    def productDescription = format.product(product: productInstance) + (inventoryItem?.lotNumber) ? " #" + inventoryItem?.lotNumber : ""
                    flash.message = "${warehouse.message(code: 'inventoryItem.addedItemToShipment.message', args: [productDescription, shipmentInstance?.name])}"
                }
            }

        } catch (ShipmentItemException e) {
            flash['errors'] = e.shipmentItem.errors
        } catch (ValidationException e) {
            flash['errors'] = e.errors
        }

        redirect(action: "showStockCard", params: ['product.id': productInstance?.id])
    }


    def saveInventoryLevel = {
        // Get existing inventory level
        def inventoryLevelInstance = InventoryLevel.get(params.id)
        def productInstance = Product.get(params?.product?.id)

        if (inventoryLevelInstance) {
            inventoryLevelInstance.properties = params
        } else {
            inventoryLevelInstance = new InventoryLevel(params)
        }

        if (!inventoryLevelInstance.hasErrors() && inventoryLevelInstance.save()) {

        } else {
            flash.message = "${warehouse.message(code: 'inventoryItem.errorSavingInventoryLevels.message')}<br/>"
            inventoryLevelInstance.errors.allErrors.each {
                flash.message += it + "<br/>"
            }
        }
        redirect(action: 'showStockCard', params: ['product.id': productInstance?.id])
    }

    def create = {
        def inventoryItem = new InventoryItem(params)
        if (InventoryItem && inventoryItem.save()) {
            flash.message = "${warehouse.message(code: 'default.created.message', args: [warehouse.message(code: 'inventoryItem.label'), params.id])}"
        } else {
            flash.message = "${warehouse.message(code: 'default.not.created.message', args: [warehouse.message(code: 'inventoryItem.label')])}"
        }
        redirect(action: 'showLotNumbers', params: ['product.id': inventoryItem?.product?.id])
    }


    def delete = {
        def inventoryItem = InventoryItem.get(params.id)
        if (inventoryItem) {
            try {
                inventoryItem.delete(flush: true)
                flash.message = "${warehouse.message(code: 'default.deleted.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Attribute'), params.id])}"
            }
            catch (org.springframework.dao.DataIntegrityViolationException e) {
                flash.message = "${warehouse.message(code: 'default.not.deleted.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Attribute'), params.id])}"
            }
        } else {
            flash.message = "${warehouse.message(code: 'default.not.found.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Attribute'), params.id])}"
        }

        redirect(action: 'showLotNumbers', params: ['product.id': inventoryItem?.product?.id])
    }

    def deleteInventoryItem = {
        def inventoryItem = InventoryItem.get(params.id)
        def productInstance = inventoryItem?.product
        def inventoryInstance = Inventory.get(inventoryItem?.inventory?.id)

        if (inventoryItem && inventoryInstance) {
            inventoryInstance.removeFromInventoryItems(inventoryItem).save()
            inventoryItem.delete()
        } else {
            inventoryItem.errors.reject("inventoryItem.error", "Could not delete inventory item")
            params.put("product.id", productInstance?.id)
            params.put("inventory.id", inventoryInstance?.id)
            log.info "Params " + params
            chain(action: "createInventoryItem", model: [inventoryItem: inventoryItem], params: params)
            return
        }
        redirect(action: 'showStockCard', params: ['product.id': productInstance?.id])

    }


    def saveTransactionEntry = {
        def productInstance = Product.get(params?.product?.id)
        if (!productInstance) {
            flash.message = "${warehouse.message(code: 'default.notfound.message', args: [warehouse.message(code: 'product.label', default: 'Product'), productInstance.id])}"
            redirect(action: "showStockCard", id: productInstance?.id)
        } else {
            def inventoryItem = inventoryService.findByProductAndLotNumber(productInstance, params.lotNumber ?: null)
            if (!inventoryItem) {
                flash.message = "${warehouse.message(code: 'default.notfound.message', args: [warehouse.message(code: 'inventoryItem.label', default: 'Inventory item'), params.lotNumber])}"
            } else {
                def transactionInstance = new Transaction(params)
                def transactionEntry = new TransactionEntry(params)

                // If we're transferring stock to another location OR consuming stock,
                // then we need to make sure the quantity is negative
                if (transactionInstance?.destination?.id != session?.warehouse?.id
                        || transactionInstance?.transactionType?.name == 'Consumption') {
                    if (transactionEntry.quantity > 0) {
                        transactionEntry.quantity = -transactionEntry.quantity
                    }
                }

                transactionEntry.inventoryItem = inventoryItem
                if (!transactionEntry.hasErrors() &&
                        transactionInstance.addToTransactionEntries(transactionEntry).save(flush: true)) {
                    flash.message = "${warehouse.message(code: 'inventoryItem.savedTransactionEntry.label')}"
                } else {
                    transactionInstance.errors.each { log.info it }
                    transactionEntry.errors.each { log.info it }
                    flash.message = "${warehouse.message(code: 'inventoryItem.inventoryItem.unableToSaveTransactionEntry.message.label')}"
                }
            }
        }
        redirect(action: "showStockCard", params: ['product.id': productInstance?.id])
    }

    def recall = {
        InventoryItem inventoryItem = InventoryItem.get(params.id)
        if (userService.isUserAdmin(session.user)) {
            if (inventoryItem.lotNumber) {
                inventoryItem.lotStatus = LotStatusCode.RECALLED
                flash.message = "${warehouse.message(code: 'inventoryItem.recall.message')}"
            } else {
                flash.message = "${warehouse.message(code: 'inventoryItem.recallError.message')}"
            }
        } else {
            flash.message = "${warehouse.message(code: 'errors.noPermissions.label')}"
        }
        redirect(action: 'showLotNumbers', params: ['product.id': inventoryItem?.product?.id])
    }

    def revertRecall = {
        InventoryItem inventoryItem = InventoryItem.get(params.id)
        if (userService.isUserAdmin(session.user)) {
            inventoryItem.lotStatus = null
            flash.message = "${warehouse.message(code: 'inventoryItem.revertRecall.message')}"
        } else {
            flash.message = "${warehouse.message(code: 'errors.noPermissions.label')}"
        }
        redirect(action: 'showLotNumbers', params: ['product.id': inventoryItem?.product?.id])
    }
}
