Disposal Return Workflow Implementation - hmislk/hmis GitHub Wiki
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.
The Disposal Return workflow follows a three-stage approval process similar to the GRN Return workflow:
- Create/Save Draft - User creates return request and can save multiple times
-
Finalize - User finalizes the return request (sets
checkedBy
) -
Authorize - Privileged user approves and completes the return (sets
completed
)
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 |
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
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
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
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
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
Scope: @SessionScoped
Key Methods:
public void saveDisposalIssueReturnBill() {
// Validate return quantities
if (!validateReturnQuantities()) {
return;
}
saveBill(); // Save bill entity
saveBillComponents(); // Save bill items
JsfUtil.addSuccessMessage("Saved");
}
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");
}
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
Scope: @SessionScoped
Key Methods:
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);
}
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);
}
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);
}
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";
}
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";
}
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"/>
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 |
Users can close (abandon) a pending return to create a new one for the same disposal issue bill.
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...");
}
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);
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>
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 |
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)
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;
}
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;
}
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 |
The system supports multiple print formats configured via ConfigOptionController
:
-
A4 Paper -
diposal_issue_return
composite component -
Custom Format 1 -
diposal_issue_return_custom_1
composite component -
Custom Format 2 -
diposal_issue_return_custom_2
composite component
Print format selection via Settings dialog (privilege: ChangeReceiptPrintingPaperTypes
).
/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
com.divudi.bean.pharmacy.IssueReturnController
com.divudi.bean.pharmacy.DisposalReturnWorkflowController
/resources/pharmacy/diposal_issue_return.xhtml
/resources/pharmacy/diposal_issue_return_custom_1.xhtml
/resources/pharmacy/diposal_issue_return_custom_2.xhtml
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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)
-
Always reload bills before processing to ensure fresh data
Bill returnBill = billService.reloadBill(disposalReturnBill);
-
Validate before state transitions
- Check
checkedBy
before allowing approval - Validate quantities before save/finalize/authorize
- Check
-
Use consistent navigation
- All pages should have same navigation buttons
- Privilege-based rendering for buttons
-
State management
- Use
printPreview
flag to toggle modes - Clear state when navigating away
- Use
-
JPQL queries
- Always filter by department
- Check
retired = false
andcancelled = false
- Use appropriate NULL checks for state fields
-
Not checking for pending returns
// ALWAYS check before creating new return if (disposalReturnWorkflowController.hasPendingDisposalReturnForSpecificBill(bill)) { // Error: Pending return exists }
-
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)
-
Not setting print preview mode
// After finalize/authorize, set print preview printPreview = true;
- 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)
- GRN Return Workflow - Similar pattern
- Bill Service
- Privilege System
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