Disposal Return Workflow Implementation - hmislk/hmis GitHub Wiki

Disposal Return Workflow - Developer Guide

Overview

This guide documents the implementation of the Pharmacy Disposal Issue Return workflow, covering the architecture, workflow states, navigation patterns, and code structure for developers working on this module.

Workflow Architecture

The Disposal Return workflow follows a three-stage approval process similar to the GRN Return workflow:

  1. Create/Save Draft - User creates return request and can save multiple times
  2. Finalize - User finalizes the return request (sets checkedBy)
  3. Authorize - Privileged user approves and completes the return (sets completed)

Bill States

State Field Condition Description
Draft checkedBy = null Not finalized User can save and edit
Finalized checkedBy != null Not completed Ready for approval
Completed completed = true Approved Fully processed return

Page Structure and Navigation

Main Pages

1. Search Page - pharmacy_search_issue_bill_for_return.xhtml

Purpose: Search and select disposal issue bills to create returns

Location: /pharmacy/pharmacy_search_issue_bill_for_return.xhtml

Key Features:

  • Search disposal issue bills by date range
  • Display bill status (Available, Partially Returned, Fully Returned)
  • Prevents creating returns for fully returned bills
  • Navigation action: issueReturnController.navigateToReturnDisposalIssueBill(bill)

Privilege Required: CreateDisposalReturn

2. Return Processing Page - pharmacy_bill_return_issue.xhtml

Purpose: Unified page for creating, finalizing, and authorizing returns

Location: /pharmacy/pharmacy_bill_return_issue.xhtml

Modes:

  • Edit Mode (printPreview = false): Show data table with editable items
  • Print Preview Mode (printPreview = true): Show receipt for printing

Action Buttons:

<!-- Save Draft - Only visible when not finalized -->
<p:commandButton
    value="Save Draft"
    action="#{issueReturnController.saveDisposalIssueReturnBill()}"
    rendered="#{issueReturnController.returnBill.checkedBy eq null
               and not issueReturnController.originalBill.fullReturned}"/>

<!-- Finalize - Only visible when not finalized -->
<p:commandButton
    value="Finalize"
    action="#{issueReturnController.finalizeDisposalIssueReturnBill()}"
    rendered="#{issueReturnController.returnBill.checkedBy eq null
               and not issueReturnController.originalBill.fullReturned}"/>

<!-- Authorize - Only visible when finalized but not completed -->
<p:commandButton
    value="Authorize Return"
    action="#{issueReturnController.settleDisposalIssueReturnBill}"
    rendered="#{issueReturnController.returnBill.checkedBy ne null
               and (issueReturnController.returnBill.completed eq false
                    or issueReturnController.returnBill.completed eq null)
               and webUserController.hasPrivilege('ApproveDisposalReturn')
               and not issueReturnController.originalBill.fullReturned}"/>

Privilege Required:

  • CreateDisposalReturn - For creating and saving
  • FinalizeDisposalReturn - For finalizing
  • ApproveDisposalReturn - For authorizing

3. Finalize List Page - pharmacy_disposal_return_finalize.xhtml

Purpose: List disposal returns that need finalization

Location: /pharmacy/pharmacy_disposal_return_finalize.xhtml

Query Logic:

// Find bills that are NOT finalized (checked = false or null)
WHERE b.checked = false OR b.checked IS NULL

Navigation: disposalReturnWorkflowController.navigateToProcessDisposalReturn(bill)

Privilege Required: FinalizeDisposalReturn

4. Approve List Page - pharmacy_disposal_return_approve.xhtml

Purpose: List disposal returns that need approval

Location: /pharmacy/pharmacy_disposal_return_approve.xhtml

Query Logic:

// Find bills that are finalized but NOT completed
WHERE b.checked = true
  AND (b.completed = false OR b.completed IS NULL)

Navigation: disposalReturnWorkflowController.navigateToProcessDisposalReturn(bill)

Privilege Required: ApproveDisposalReturn

5. Completed Returns Page - pharmacy_disposal_return_completed.xhtml

Purpose: List completed disposal returns with print functionality

Location: /pharmacy/pharmacy_disposal_return_completed.xhtml

Query Logic:

// Find bills that are completed
WHERE b.completed = true
ORDER BY b.completedAt DESC

Navigation: disposalReturnWorkflowController.navigateToPrintCompletedDisposalReturn(bill)

Privilege Required: ViewDisposalReturn

Controller Architecture

IssueReturnController

Scope: @SessionScoped

Key Methods:

saveDisposalIssueReturnBill()

public void saveDisposalIssueReturnBill() {
    // Validate return quantities
    if (!validateReturnQuantities()) {
        return;
    }
    saveBill();           // Save bill entity
    saveBillComponents(); // Save bill items
    JsfUtil.addSuccessMessage("Saved");
}

finalizeDisposalIssueReturnBill()

public void finalizeDisposalIssueReturnBill() {
    // Validate and save first
    if (!validateReturnQuantities()) {
        return;
    }
    saveDisposalIssueReturnBill();

    // Mark as finalized
    getReturnBill().setEditedAt(new Date());
    getReturnBill().setEditor(sessionController.getLoggedUser());
    getReturnBill().setChecked(true);
    getReturnBill().setCheckeAt(new Date());
    getReturnBill().setCheckedBy(sessionController.getLoggedUser());

    getBillFacade().edit(getReturnBill());
    printPreview = true;  // Switch to print preview mode
    JsfUtil.addSuccessMessage("Finalized");
}

settleDisposalIssueReturnBill()

public void settleDisposalIssueReturnBill() {
    // Validate bill is finalized
    if (getReturnBill().getCheckedBy() == null) {
        JsfUtil.addErrorMessage("Please Finalize Bill First. Cannot Return");
        return;
    }

    // Validate quantities
    if (!hasQtyToReturn(returnBillItems)) {
        JsfUtil.addErrorMessage("Return Quantity is Zero. Cannot Return");
        return;
    }

    if (!validateReturnQuantities()) {
        return;
    }

    // Save as completed
    calculateBillTotal();
    saveSettlingBill();          // Sets completed = true
    saveSettlingBillComponents();

    // Update references
    getReturnBill().setReferenceBill(getOriginalBill());
    getBillFacade().edit(getReturnBill());

    getOriginalBill().setRefundedBill(getReturnBill());
    getOriginalBill().setRefunded(true);
    getOriginalBill().getRefundBills().add(getReturnBill());
    getBillFacade().edit(getOriginalBill());

    printPreview = true;
    JsfUtil.addSuccessMessage("Successfully Returned");
}

Key Properties:

private Bill originalBill;           // The original disposal issue bill
private Bill returnBill;             // The return bill being created
private List<BillItem> returnBillItems;
private boolean printPreview;        // Toggle between edit/print mode

DisposalReturnWorkflowController

Scope: @SessionScoped

Key Methods:

fillDisposalReturnsToFinalize()

public void fillDisposalReturnsToFinalize() {
    Map<String, Object> params = new HashMap<>();
    String jpql = "SELECT b FROM Bill b "
        + "WHERE b.retired = :retired "
        + "AND b.billTypeAtomic = :billTypeAtomic "
        + "AND b.department = :department "
        + "AND (b.checked = :checked OR b.checked IS NULL) "
        + "AND (b.cancelled = :cancelled OR b.cancelled IS NULL) "
        + "ORDER BY b.createdAt DESC";

    params.put("retired", false);
    params.put("billTypeAtomic", BillTypeAtomic.PHARMACY_DISPOSAL_ISSUE_RETURN);
    params.put("department", sessionController.getDepartment());
    params.put("checked", false);
    params.put("cancelled", false);

    disposalReturnsToFinalize = billFacade.findByJpql(jpql, params);
}

fillDisposalReturnsToApprove()

public void fillDisposalReturnsToApprove() {
    Map<String, Object> params = new HashMap<>();
    String jpql = "SELECT b FROM Bill b "
        + "WHERE b.retired = :retired "
        + "AND b.billTypeAtomic = :billTypeAtomic "
        + "AND b.department = :department "
        + "AND b.checked = :checked "
        + "AND (b.cancelled = :cancelled OR b.cancelled IS NULL) "
        + "AND (b.completed = :completed OR b.completed IS NULL) "
        + "ORDER BY b.checkeAt DESC";

    params.put("retired", false);
    params.put("billTypeAtomic", BillTypeAtomic.PHARMACY_DISPOSAL_ISSUE_RETURN);
    params.put("department", sessionController.getDepartment());
    params.put("checked", true);
    params.put("cancelled", false);
    params.put("completed", false);

    disposalReturnsToApprove = billFacade.findByJpql(jpql, params);
}

fillCompletedDisposalReturns()

public void fillCompletedDisposalReturns() {
    Map<String, Object> params = new HashMap<>();
    String jpql = "SELECT b FROM Bill b "
        + "WHERE b.retired = :retired "
        + "AND b.billTypeAtomic = :billTypeAtomic "
        + "AND b.department = :department "
        + "AND b.completed = :completed "
        + "AND (b.cancelled = :cancelled OR b.cancelled IS NULL) "
        + "ORDER BY b.completedAt DESC";

    params.put("retired", false);
    params.put("billTypeAtomic", BillTypeAtomic.PHARMACY_DISPOSAL_ISSUE_RETURN);
    params.put("department", sessionController.getDepartment());
    params.put("completed", true);
    params.put("cancelled", false);

    completedDisposalReturns = billFacade.findByJpql(jpql, params);
}

navigateToProcessDisposalReturn()

public String navigateToProcessDisposalReturn(Bill disposalReturnBill) {
    if (disposalReturnBill == null || disposalReturnBill.getReferenceBill() == null) {
        JsfUtil.addErrorMessage("No disposal return bill selected");
        return null;
    }

    // Reload bills to get fresh data
    Bill returnBill = billService.reloadBill(disposalReturnBill);
    Bill originalBill = billService.reloadBill(disposalReturnBill.getReferenceBill());

    // Set in IssueReturnController
    issueReturnController.setReturnBill(returnBill);
    issueReturnController.setOriginalBill(originalBill);
    issueReturnController.setReturnBillItems(returnBill.getBillItems());
    issueReturnController.setOriginalBillItems(originalBill.getBillItems());

    // Navigate to processing page
    return "/pharmacy/pharmacy_bill_return_issue?faces-redirect=true";
}

navigateToPrintCompletedDisposalReturn()

public String navigateToPrintCompletedDisposalReturn(Bill completedReturnBill) {
    if (completedReturnBill == null || completedReturnBill.getReferenceBill() == null) {
        JsfUtil.addErrorMessage("No disposal return bill selected");
        return null;
    }

    // Reload bills
    Bill returnBill = billService.reloadBill(completedReturnBill);
    Bill originalBill = billService.reloadBill(completedReturnBill.getReferenceBill());

    // Set in IssueReturnController
    issueReturnController.setReturnBill(returnBill);
    issueReturnController.setOriginalBill(originalBill);
    issueReturnController.setReturnBillItems(returnBill.getBillItems());
    issueReturnController.setOriginalBillItems(originalBill.getBillItems());

    // Enable print preview mode
    issueReturnController.setPrintPreview(true);

    // Navigate to print preview
    return "/pharmacy/pharmacy_bill_return_issue?faces-redirect=true";
}

Navigation Buttons Pattern

All workflow pages include consistent navigation buttons in the header:

<p:commandButton
    value="List Issues To Create Return"
    action="/pharmacy/pharmacy_search_issue_bill_for_return?faces-redirect=true"
    icon="fas fa-plus-circle"
    styleClass="ui-button-info ui-button-outlined"
    rendered="#{webUserController.hasPrivilege('CreateDisposalReturn')}"
    title="Create a new Disposal Return Request"/>

<p:commandButton
    value="List Returns to Finalize"
    action="/pharmacy/pharmacy_disposal_return_finalize?faces-redirect=true"
    icon="fas fa-tasks"
    styleClass="ui-button-info ui-button-outlined"
    rendered="#{webUserController.hasPrivilege('FinalizeDisposalReturn')}"
    title="Finalize Disposal Return Requests"/>

<p:commandButton
    value="List Returns To Approve"
    action="/pharmacy/pharmacy_disposal_return_approve?faces-redirect=true"
    icon="fas fa-check-circle"
    styleClass="ui-button-info ui-button-outlined"
    rendered="#{webUserController.hasPrivilege('ApproveDisposalReturn')}"
    title="Approve Disposal Return Requests"/>

<p:commandButton
    value="Completed Returns"
    action="/pharmacy/pharmacy_disposal_return_completed?faces-redirect=true"
    icon="fas fa-check-double"
    styleClass="ui-button-info ui-button-outlined"
    rendered="#{webUserController.hasPrivilege('ViewDisposalReturn')}"
    title="View Completed Disposal Returns"/>

Database Schema

Key Fields

Field Type Purpose
billTypeAtomic Enum PHARMACY_DISPOSAL_ISSUE_RETURN
referenceBill Bill Original disposal issue bill
checked Boolean Finalized flag
checkedBy WebUser User who finalized
checkeAt Date Finalization timestamp
completed Boolean Approved/completed flag
completedBy WebUser User who approved
completedAt Date Approval timestamp
cancelled Boolean Cancellation flag (not used for returns)
billClosed Boolean Closed/abandoned flag
retired Boolean Soft delete flag

Close/Abandon Return Functionality

Users can close (abandon) a pending return to create a new one for the same disposal issue bill.

Close Method

Method: closeDisposalReturn(Bill returnBillToClose) Location: DisposalReturnWorkflowController.java:314

public void closeDisposalReturn(Bill returnBillToClose) {
    // Validate bill can be closed
    if (returnBillToClose.isCompleted()) {
        JsfUtil.addErrorMessage("Cannot close a completed disposal return");
        return;
    }
    if (returnBillToClose.isBillClosed()) {
        JsfUtil.addErrorMessage("This disposal return is already closed");
        return;
    }

    // Mark as closed
    returnBillToClose.setBillClosed(true);
    billFacade.edit(returnBillToClose);

    // Refresh lists
    fillDisposalReturnsToFinalize();
    fillDisposalReturnsToApprove();

    JsfUtil.addSuccessMessage("Disposal return closed successfully...");
}

Updated Validation Query

The hasPendingDisposalReturnForSpecificBill() method excludes closed bills:

String jpql = "SELECT COUNT(b) FROM Bill b "
    + "WHERE b.retired = :retired "
    + "AND b.billTypeAtomic = :billTypeAtomic "
    + "AND b.department = :department "
    + "AND b.referenceBill = :referenceBill "
    + "AND (b.cancelled = :cancelled OR b.cancelled IS NULL) "
    + "AND (b.billClosed = :closed OR b.billClosed IS NULL) "  // Exclude closed
    + "AND (b.completed = :completed OR b.completed IS NULL) ";

params.put("closed", false);

UI Implementation

Close Button Pattern:

  • Style: ui-button-danger ui-button-outlined (red outlined)
  • Icon: fas fa-times (X icon)
  • Confirmation: Always require confirmation
  • Width: min-width: 70px

Finalize Page (pharmacy_disposal_return_finalize.xhtml:224):

<p:commandButton
    id="close"
    value="Close"
    icon="fas fa-times"
    class="ui-button-danger ui-button-outlined"
    action="#{disposalReturnWorkflowController.closeDisposalReturn(b)}"
    rendered="#{b.billClosed ne true and b.checkedBy eq null}"
    disabled="#{b.billClosed eq true or b.checkedBy ne null}">
    <p:confirm header="Confirm Close"
               message="Are you sure you want to close this disposal return?"/>
</p:commandButton>

Approve Page (pharmacy_disposal_return_approve.xhtml:226):

<p:commandButton
    id="close"
    value="Close"
    icon="fas fa-times"
    class="ui-button-danger ui-button-outlined"
    action="#{disposalReturnWorkflowController.closeDisposalReturn(b)}"
    rendered="#{b.billClosed ne true and b.completed ne true and b.checkedBy ne null}"
    disabled="#{b.billClosed eq true or b.completed eq true}">
    <p:confirm header="Confirm Close"
               message="Are you sure you want to close this finalized disposal return?"/>
</p:commandButton>

Status Badge:

<h:panelGroup rendered="#{b.billClosed}" styleClass="d-block mt-1">
    <small class="text-muted fst-italic">
        <i class="fas fa-times-circle me-1"></i>Closed
    </small>
</h:panelGroup>

Close vs Cancel

Why billClosed instead of cancelled?

Aspect cancelled billClosed
Purpose Cancel actual transactions Abandon pending requests
Complexity Creates CancelledBill object Simple boolean flag
Semantics Permanent cancellation Temporary abandonment
Stock Impact May reverse stock movements No stock impact (not yet completed)
Use Case Approved/completed bills Draft/pending returns

Workflow States with Close

State Field Values Can Close? Close Button Location
Draft checkedBy = null โœ… Yes Finalize page
Finalized checkedBy != null, completed = null โœ… Yes Approve page
Completed completed = true โŒ No N/A
Closed billClosed = true โŒ No N/A (already closed)

Close Button Rendering Logic:

  • Finalize Page: Show when billClosed ne true AND checkedBy eq null (draft only)
  • Approve Page: Show when billClosed ne true AND completed ne true AND checkedBy ne null (finalized only)

Validation Logic

Return Quantity Validation

public boolean validateReturnQuantities() {
    if (returnBillItems == null || returnBillItems.isEmpty()) {
        JsfUtil.addErrorMessage("No items found to return");
        return false;
    }

    for (BillItem returnItem : returnBillItems) {
        if (returnItem == null || returnItem.getReferanceBillItem() == null) {
            continue;
        }

        double requestedReturnQty = returnItem.getQty();
        if (requestedReturnQty <= 0) {
            continue; // Skip zero quantities
        }

        // Validate against original issued quantity and previous returns
        if (!validateItemReturnQuantity(returnItem)) {
            return false;
        }
    }

    return true;
}

Preventing Duplicate Returns

public boolean hasPendingDisposalReturnForSpecificBill(Bill disposalIssueBill) {
    String jpql = "SELECT COUNT(b) FROM Bill b "
        + "WHERE b.retired = :retired "
        + "AND b.billTypeAtomic = :billTypeAtomic "
        + "AND b.department = :department "
        + "AND b.referenceBill = :referenceBill "
        + "AND b.checked = :checked "
        + "AND (b.cancelled = :cancelled OR b.cancelled IS NULL) "
        + "AND (b.completed = :completed OR b.completed IS NULL)";

    // ... parameters

    Long count = billFacade.countByJpql(jpql, params);
    return count != null && count > 0;
}

Privileges

Privilege Purpose Required For
CreateDisposalReturn Create and save returns Search page, Save Draft, Finalize
FinalizeDisposalReturn Finalize returns Finalize list page
ApproveDisposalReturn Approve/authorize returns Approve list page, Authorize button
ViewDisposalReturn View completed returns Completed returns page

Print Templates

The system supports multiple print formats configured via ConfigOptionController:

  1. A4 Paper - diposal_issue_return composite component
  2. Custom Format 1 - diposal_issue_return_custom_1 composite component
  3. Custom Format 2 - diposal_issue_return_custom_2 composite component

Print format selection via Settings dialog (privilege: ChangeReceiptPrintingPaperTypes).

File Locations

XHTML Pages

  • /pharmacy/pharmacy_search_issue_bill_for_return.xhtml
  • /pharmacy/pharmacy_bill_return_issue.xhtml
  • /pharmacy/pharmacy_disposal_return_finalize.xhtml
  • /pharmacy/pharmacy_disposal_return_approve.xhtml
  • /pharmacy/pharmacy_disposal_return_completed.xhtml

Java Controllers

  • com.divudi.bean.pharmacy.IssueReturnController
  • com.divudi.bean.pharmacy.DisposalReturnWorkflowController

Composite Components

  • /resources/pharmacy/diposal_issue_return.xhtml
  • /resources/pharmacy/diposal_issue_return_custom_1.xhtml
  • /resources/pharmacy/diposal_issue_return_custom_2.xhtml

Workflow Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Disposal Return Workflow                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

1. SELECT DISPOSAL ISSUE BILL
   pharmacy_search_issue_bill_for_return.xhtml
   โ†“
   issueReturnController.navigateToReturnDisposalIssueBill(bill)
   โ†“

2. CREATE/EDIT RETURN
   pharmacy_bill_return_issue.xhtml (Edit Mode)
   โ”œโ”€ Save Draft โ†’ saveDisposalIssueReturnBill()
   โ”‚               (checked = null)
   โ”‚
   โ””โ”€ Finalize โ†’ finalizeDisposalIssueReturnBill()
                 (checked = true, checkedBy = user, printPreview = true)
   โ†“

3. FINALIZE WORKFLOW
   pharmacy_disposal_return_finalize.xhtml
   โ”œโ”€ Lists: checked = false OR null
   โ””โ”€ Edit โ†’ navigateToProcessDisposalReturn(bill)
             โ†’ Returns to Step 2
   โ†“

4. APPROVE WORKFLOW
   pharmacy_disposal_return_approve.xhtml
   โ”œโ”€ Lists: checked = true AND completed = false/null
   โ””โ”€ Approve โ†’ navigateToProcessDisposalReturn(bill)
                โ†’ pharmacy_bill_return_issue.xhtml
                โ†’ settleDisposalIssueReturnBill()
                   (completed = true, completedBy = user)
   โ†“

5. COMPLETED RETURNS
   pharmacy_disposal_return_completed.xhtml
   โ”œโ”€ Lists: completed = true
   โ””โ”€ Print โ†’ navigateToPrintCompletedDisposalReturn(bill)
              โ†’ pharmacy_bill_return_issue.xhtml (Print Preview Mode)

Best Practices

For Developers

  1. Always reload bills before processing to ensure fresh data

    Bill returnBill = billService.reloadBill(disposalReturnBill);
  2. Validate before state transitions

    • Check checkedBy before allowing approval
    • Validate quantities before save/finalize/authorize
  3. Use consistent navigation

    • All pages should have same navigation buttons
    • Privilege-based rendering for buttons
  4. State management

    • Use printPreview flag to toggle modes
    • Clear state when navigating away
  5. JPQL queries

    • Always filter by department
    • Check retired = false and cancelled = false
    • Use appropriate NULL checks for state fields

Common Pitfalls

  1. Not checking for pending returns

    // ALWAYS check before creating new return
    if (disposalReturnWorkflowController.hasPendingDisposalReturnForSpecificBill(bill)) {
        // Error: Pending return exists
    }
  2. Incorrect state queries

    // WRONG: Only checks false
    WHERE b.checked = false
    
    // CORRECT: Checks both false and null
    WHERE (b.checked = false OR b.checked IS NULL)
  3. Not setting print preview mode

    // After finalize/authorize, set print preview
    printPreview = true;

Testing Checklist

  • Create new disposal return from search page
  • Save draft multiple times
  • Validate quantity restrictions
  • Finalize return request
  • View in finalize list
  • Edit finalized return (should navigate to view mode)
  • Approve return from approve list
  • Verify completed flag set
  • View completed return in completed list
  • Print completed return
  • Test all navigation buttons
  • Verify privilege-based access control
  • Test with fully returned bills (should prevent new returns)
  • Test with pending returns (should prevent duplicate returns)

Related Documentation

Change Log

Date Version Changes
2025-01-03 1.0 Initial documentation - Disposal return workflow implementation with navigation buttons and completed returns page

Maintained By: Development Team Last Updated: January 3, 2025

โš ๏ธ **GitHub.com Fallback** โš ๏ธ