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. } def movementResult = inventoryMovementService.reverseMove(inventoryMovements[0]) if(movementResult.error) return fail(code:"inventoryMovement.quantity.insufficientItemsInStock") 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") } // 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