/* * The contents of this file are subject to the OpenMRS Public License * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://license.openmrs.org * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and * limitations under the License. * * Copyright (C) OpenHMIS. All Rights Reserved. */ package org.openmrs.module.openhmis.cashier.api.util; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.util.List; import org.apache.commons.logging.Log; import org.openmrs.GlobalProperty; import org.openmrs.api.APIException; import org.openmrs.api.AdministrationService; import org.openmrs.api.context.Context; import org.openmrs.logic.op.In; import org.openmrs.messagesource.MessageSourceService; import org.openmrs.module.openhmis.cashier.ModuleSettings; import org.openmrs.module.openhmis.cashier.api.ICashierOptionsService; import org.openmrs.module.openhmis.cashier.api.model.Bill; import org.openmrs.module.openhmis.cashier.api.model.BillLineItem; import org.openmrs.module.openhmis.cashier.api.model.CashierOptions; import org.openmrs.module.openhmis.inventory.api.IDepartmentDataService; import org.openmrs.module.openhmis.inventory.api.IItemDataService; import org.openmrs.module.openhmis.inventory.api.model.Department; import org.openmrs.module.openhmis.inventory.api.model.Item; import org.openmrs.module.openhmis.inventory.api.model.ItemPrice; /** * Utility class for Rounding off bill values */ public class RoundingUtil { protected RoundingUtil() {} public static BigDecimal round(BigDecimal value, Integer nearest, CashierOptions.RoundingMode mode) { if (nearest == null || nearest.equals(0)) { return value; } double valueD = value.doubleValue(); switch (mode) { case FLOOR: return new BigDecimal(nearest * (Math.floor(Math.abs(valueD / nearest)))); case CEILING: return new BigDecimal(nearest * (Math.ceil(Math.abs(valueD / nearest)))); case MID: return new BigDecimal(nearest * (Math.round(valueD / nearest))); default: return value; } } public static void setupRoundingDeptAndItem(Log log) { /* * Automatically add rounding item & department */ AdministrationService adminService = Context.getService(AdministrationService.class); String nearest = adminService.getGlobalProperty(ModuleSettings.ROUND_TO_NEAREST_PROPERTY); if (nearest != null && !nearest.isEmpty() && !nearest.equals("0")) { MessageSourceService msgService = Context.getMessageSourceService(); IDepartmentDataService deptService = Context.getService(IDepartmentDataService.class); IItemDataService itemService = Context.getService(IItemDataService.class); Integer deptId = parseDepartmentId(adminService); Integer itemId = parseItemId(adminService); String name = msgService.getMessage("openhmis.cashier.rounding.itemName"); String description = msgService.getMessage("openhmis.cashier.rounding.itemDescription"); Department department = null; if (deptId == null) { department = new Department(); department.setName(name); department.setDescription(description); department.setRetired(true); department.setRetireReason("Used by Cashier Module for rounding adjustments."); deptService.save(department); log.info("Created department for rounding item (ID = " + department.getId() + ")..."); adminService.saveGlobalProperty(new GlobalProperty(ModuleSettings.ROUNDING_DEPT_ID, department.getId() .toString())); } if (itemId == null) { Item item = new Item(); item.setName(name); item.setDescription(description); if (department == null) { department = Context.getService(IDepartmentDataService.class).getById(deptId); if (department == null) { throw new APIException( "Department with id " + deptId + " doesn't exist."); } } item.setDepartment(department); item.setHasExpiration(false); ItemPrice price = item.addPrice(name, BigDecimal.ZERO); item.setDefaultPrice(price); itemService.save(item); log.info("Created item for rounding (ID = " + item.getId() + ")..."); adminService .saveGlobalProperty(new GlobalProperty(ModuleSettings.ROUNDING_ITEM_ID, item.getId().toString())); } } } /** * Add a rounding line item to a bill if necessary * @param bill * @should handle a rounding line item (add/update/delete with the appropriate value) * @should not modify a bill that needs no rounding * @should round bills with a non zero amount correctly for MID * @should round bills with a non zero amount correctly for CEILING * @should round bills with a non zero amount correctly for FLOOR */ public static void handleRoundingLineItem(Bill bill) { ICashierOptionsService cashOptService = Context.getService(ICashierOptionsService.class); CashierOptions options = cashOptService.getOptions(); if (options.getRoundToNearest().equals(BigDecimal.ZERO)) { return; } if (options.getRoundingItemUuid() == null) { throw new APIException( "No rounding item specified in options. This must be set in order to use rounding for bill totals."); } // Get rounding item IItemDataService itemService = Context.getService(IItemDataService.class); Item roundingItem = itemService.getByUuid(options.getRoundingItemUuid()); BillLineItem roundingLineItem = findRoundingLineItem(bill, roundingItem); BigDecimal difference = calculateRoundingValue(bill, options, roundingLineItem); if (difference.equals(BigDecimal.ZERO) && roundingLineItem != null) { bill.removeLineItem(roundingLineItem); } else if (!difference.equals(BigDecimal.ZERO) && roundingLineItem == null) { // Create line item for rounding item and the required amount bill.addLineItem(roundingItem, difference.abs(), "", difference.compareTo(BigDecimal.ZERO) > 0 ? -1 : 1); } else if (!difference.equals(BigDecimal.ZERO)) { updateRoundingItem(bill, difference, roundingLineItem); } bill.recalculateLineItemOrder(); } private static BillLineItem findRoundingLineItem(Bill bill, Item roundingItem) { BillLineItem result = null; for (BillLineItem lineItem : bill.getLineItems()) { if (roundingItem.equals(lineItem.getItem())) { result = lineItem; break; } } return result; } private static void updateRoundingItem(Bill bill, BigDecimal difference, BillLineItem roundingLineItem) { roundingLineItem.setPrice(difference.abs()); roundingLineItem.setQuantity(difference.compareTo(BigDecimal.ZERO) > 0 ? -1 : 1); bill.removeLineItem(roundingLineItem); bill.addLineItem(roundingLineItem); } private static BigDecimal calculateRoundingValue(Bill bill, CashierOptions options, BillLineItem roundingLineItem) { List<BillLineItem> lineItems = bill.getLineItems(); BigDecimal itemTotal = new BigDecimal(0); if (lineItems == null) { return BigDecimal.ZERO; } for (BillLineItem lineItem : lineItems) { if (lineItem != null && !lineItem.getVoided()) { if (roundingLineItem == null || !roundingLineItem.equals(lineItem)) { itemTotal = itemTotal.add(lineItem.getTotal()); } } } return itemTotal.subtract(RoundingUtil.round(itemTotal, options.getRoundToNearest(), options.getRoundingMode())); } private static Integer parseItemId(AdministrationService adminService) { Integer itemId; try { itemId = Integer.parseInt(adminService.getGlobalProperty(ModuleSettings.ROUNDING_ITEM_ID)); } catch (NumberFormatException e) { itemId = null; } return itemId; } private static Integer parseDepartmentId(AdministrationService adminService) { Integer deptId; try { deptId = Integer.parseInt(adminService.getGlobalProperty(ModuleSettings.ROUNDING_DEPT_ID)); } catch (NumberFormatException e) { deptId = null; } return deptId; } }