diff --git a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java index 5100c612..cc472b3c 100644 --- a/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/billing/api/impl/BillServiceImpl.java @@ -22,8 +22,11 @@ import java.net.URL; import java.security.AccessControlException; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.itextpdf.io.font.constants.StandardFonts; import com.itextpdf.io.image.ImageDataFactory; @@ -100,6 +103,58 @@ protected IEntityAuthorizationPrivileges getPrivileges() { protected void validate(Bill bill) { } + private void mergeDuplicateLineItems(Bill bill) { + if (bill == null || bill.getLineItems() == null || bill.getLineItems().isEmpty()) { + return; + } + + List lineItems = bill.getLineItems(); + List mergedItems = new ArrayList<>(); + Map itemMap = new HashMap<>(); + + for (BillLineItem item : lineItems) { + if (item == null) { + continue; + } + + String compositeKey = buildCompositeKey(item); + + if (itemMap.containsKey(compositeKey)) { + BillLineItem existingItem = itemMap.get(compositeKey); + int newQuantity = existingItem.getQuantity() + item.getQuantity(); + existingItem.setQuantity(newQuantity); + + LOG.debug("Merged duplicate line item with key: " + compositeKey + ", new quantity: " + newQuantity); + } else { + itemMap.put(compositeKey, item); + mergedItems.add(item); + } + } + + bill.getLineItems().clear(); + bill.getLineItems().addAll(mergedItems); + } + + private String buildCompositeKey(BillLineItem item) { + StringBuilder key = new StringBuilder(); + + if (item.getBillableService() != null) { + key.append("service:").append(item.getBillableService().getUuid()); + } else if (item.getItem() != null) { + key.append("stock:").append(item.getItem().getUuid()); + } + + if (item.getItemPrice() != null) { + key.append("|price:").append(item.getItemPrice().getUuid()); + } + + if (item.getPrice() != null) { + key.append("|amount:").append(item.getPrice().toPlainString()); + } + + return key.toString(); + } + /** * Saves the bill to the database, creating a new bill or updating an existing one. * @@ -117,6 +172,9 @@ public Bill save(Bill bill) { throw new NullPointerException("The bill must be defined."); } + // Merge duplicate line items before any other processing + mergeDuplicateLineItems(bill); + // Check for refund. // A refund is given when the total of the bill's line items is negative. if (bill.getTotal().compareTo(BigDecimal.ZERO) < 0 && !Context.hasPrivilege(PrivilegeConstants.REFUND_MONEY)) { @@ -133,7 +191,6 @@ public Bill save(Bill bill) { bill.setReceiptNumber(generator.generateNumber(bill)); } } - // Check if there is an existing pending bill for the patient List bills = searchBill(bill.getPatient()); if (!bills.isEmpty()) { Bill billToUpdate = bills.get(0); @@ -143,18 +200,17 @@ public Bill save(Bill bill) { billToUpdate.getLineItems().add(item); } - // Calculate the total payments made on the bill + mergeDuplicateLineItems(billToUpdate); + BigDecimal totalPaid = billToUpdate.getPayments().stream().map(Payment::getAmountTendered) .reduce(BigDecimal.ZERO, BigDecimal::add); - // Check if the bill is fully paid if (totalPaid.compareTo(billToUpdate.getTotal()) >= 0) { billToUpdate.setStatus(BillStatus.PAID); } else { billToUpdate.setStatus(BillStatus.PENDING); } - // Save the updated bill return super.save(billToUpdate); } @@ -228,10 +284,6 @@ public void apply(Criteria criteria) { }); } - /* - These methods are overridden to ensure that any null line items (created as part of a bug in 1.7.0) are removed - from the results before being returned to the caller. - */ @Override public List getAll(boolean includeVoided, PagingInfo pagingInfo) { List results = super.getAll(includeVoided, pagingInfo); @@ -267,8 +319,7 @@ public File downloadBillReceipt(Bill bill) { .concat(patient.getFamilyName() != null ? bill.getPatient().getFamilyName() : "").concat(" ") .concat(patient.getMiddleName() != null ? bill.getPatient().getMiddleName() : ""); String gender = patient.getGender() != null ? patient.getGender() : ""; - String dob = patient.getBirthdate() != null - ? Utils.getSimpleDateFormat("dd-MMM-yyyy").format(patient.getBirthdate()) + String dob = patient.getBirthdate() != null ? Utils.getSimpleDateFormat("dd-MMM-yyyy").format(patient.getBirthdate()) : ""; File returnFile; diff --git a/omod/src/main/resources/liquibase.xml b/omod/src/main/resources/liquibase.xml index ca038697..f22dd3b3 100644 --- a/omod/src/main/resources/liquibase.xml +++ b/omod/src/main/resources/liquibase.xml @@ -869,6 +869,71 @@ + + + + + + + Add unique constraint to prevent duplicate line items with same service, price, and item price + + + + CREATE TEMPORARY TABLE IF NOT EXISTS temp_duplicates AS + SELECT + bill_id, + COALESCE(service_id, -1) as service_id_val, + COALESCE(item_id, -1) as item_id_val, + COALESCE(price_id, -1) as price_id_val, + price, + MIN(bill_line_item_id) as keep_id, + SUM(quantity) as total_quantity + FROM cashier_bill_line_item + WHERE voided = 0 + GROUP BY bill_id, COALESCE(service_id, -1), COALESCE(item_id, -1), COALESCE(price_id, -1), price + HAVING COUNT(*) > 1; + + + + + UPDATE cashier_bill_line_item bli + INNER JOIN temp_duplicates td ON bli.bill_line_item_id = td.keep_id + SET bli.quantity = td.total_quantity; + + + + + UPDATE cashier_bill_line_item bli + INNER JOIN temp_duplicates td ON + bli.bill_id = td.bill_id + AND COALESCE(bli.service_id, -1) = td.service_id_val + AND COALESCE(bli.item_id, -1) = td.item_id_val + AND COALESCE(bli.price_id, -1) = td.price_id_val + AND bli.price = td.price + AND bli.bill_line_item_id != td.keep_id + SET bli.voided = 1, + bli.void_reason = 'Duplicate line item merged during database migration', + bli.date_voided = NOW(), + bli.voided_by = 1 + WHERE bli.voided = 0; + + + + + DROP TEMPORARY TABLE IF EXISTS temp_duplicates; + + + + + + + + + + + + + Migrate global properties from 'cashier.*' prefix to 'billing.*'