Lokasi ngalangkungan proxy:   [ UP ]  
[Ngawartoskeun bug]   [Panyetelan cookie]                
Skip to content

hotfix(pharmacy): coop — PO request PBI fixes + stock-integrity fixes from #21401#21422

Open
buddhika75 wants to merge 19 commits into
coop-prodfrom
21417-po-pbi-guard-hotfix
Open

hotfix(pharmacy): coop — PO request PBI fixes + stock-integrity fixes from #21401#21422
buddhika75 wants to merge 19 commits into
coop-prodfrom
21417-po-pbi-guard-hotfix

Conversation

@buddhika75

@buddhika75 buddhika75 commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Hotfix for coop production combining two fix sets:

  1. PO request duplicate-billitem / zero-qty PBI fix (issue PO request double-submit creates duplicate BillItem sharing one BIFD; surviving line ends with zero-qty PharmaceuticalBillItem, blocking GRN #21417, merged to development via fix(pharmacy): prevent duplicate PO request bill items and self-heal zero-qty PBIs #21420) — the bug that blocked GRN for POR/COOP/MP/26/000965 (Maxgalin 50mg qty 0; incident record [COOP Production] GRN blocked for POR/COOP/MP/26/000965 — Maxgalin 50mg qty 0 (PBI corrupted by duplicate-line removal) — data corrected 2026-06-11 #21418)
  2. Pharmacy stock-integrity fixes from the Ruhunu hotfix Ruhunu hotfix: pharmacy stock-integrity fixes (#21266 family — all 6 root causes) #21401 (fix(pharmacy): duplicate BillItems on transfer receive due to JPA cascade double-insert #21266 family — all 6 root causes), cherry-picked onto coop-prod

Part 1 — PO request fix (cherry-pick of #21420, commit b544eb2d58)

  • saveRequestWithoutMessage() / finalizeRequest() synchronized — concurrent saves from one session persisted the same in-memory BillItem twice (duplicate rows sharing one BIFD)
  • resyncPharmaceuticalBillItemIfEmpty() at save/finalize — rebuilds an all-zero PBI from the finance details so a PO can no longer reach approval/GRN with qty 0
  • Enter-key guards on the PO request page (qty/price inputs + autocompletes)

Part 2 — stock-integrity fixes (cherry-picked from #21401)

Fix Commit (this branch)
Disposal Consumption Report: net consumption 21b4f86fc6
Block GRN Return approval when stock insufficient 8ac5f7b333
Pre-deduction stock check in updateStock() 6eb34746b1
Duplicate BillItems on transfer receive (cascade re-insert) + a11y 30ca20091e
Transfer receive review fixes (settling guard, edit-after-fillData, try/finally restore) 9c76b53fc6
COGS report: date by stock-move date, remove double-counts 2d85c5c26d
Phantom transfer receives bound to issuing dept stock 388775defb
Stale-list Close clobbering settled disposal returns 8b7993b077
36 missing Privileges enum constants (General Stores login) df77820070
Transfer issue: move stock by user-entered qty 7428587104
Orphan-removal guard on disposal return merges fa4478ac97
Status panels explaining hidden disposal-return buttons 8a6ad187a1
GRN return approval: exact finalized quantities only 0083304f0e
Direct purchase return approval: exact finalized quantities only 8965b065c6
Count stock-take adjustment bills in COGS 607934adc9
Temporarily disable GRN Return Cancel on reprint (#21266) 7e8a3cc16c
Temporarily disable Transfer Receive / cashier sale cancel (#21419) 93e3e14229

Deliberately NOT cherry-picked:

  • 375f1336a2 (separate ID allocators for dual-version operation) — Ruhunu-specific; the commit itself notes coop is already on IDENTITY ID generation
  • 3dffd3449f (merge) + 3a8083cbdc (remove duplicate helpers left by that merge) — merge artifacts; this branch took the consumption-report changes via the original commit, so there are no duplicate helpers to remove (verified: single copy of consumptionValueSign/consumptionQtySign)

Conflict resolutions & verification

Three conflicts were resolved (TransferReceiveController ×2, Privileges ×1), caused by cherry-pick order and pre-existing coop/ruhunu prod divergence. Verification: every touched file was diffed against pharmacy-stock-fixes-hotfixTransferReceiveController, all four return/issue workflow controllers, and all pharmacy XHTML pages converge to byte-identical content; the remaining per-file differences match the pre-existing coop-prodruhunu-prod divergence exactly (i.e., only Ruhunu-only history, nothing lost, nothing leaked).

The Part 1 fixes were manually tested on a local deployment with DB verification; Part 2 fixes are QA-verified on the Ruhunu production build per #21401. Recommend a compile + smoke test of transfer receive and GRN/direct-purchase return approval on coop staging before merge.

🤖 Generated with Claude Code

…zero-qty PBIs

A double-submit race on the purchase order request page (Enter-key
defaultCommand firing btnSave while another save was in flight) persisted
the same in-memory BillItem twice, creating duplicate rows that share one
BillItemFinanceDetails with only one of them owning the
PharmaceuticalBillItem. Removing the visible duplicate could retire the
row holding the real PBI; the survivor then got a lazily created all-zero
PBI, and PO approval/GRN (which reads pbi.qty) showed qty 0 and blocked
the GRN (coop POR/COOP/MP/26/000965, Maxgalin 50mg).

Server side (PurchaseOrderRequestController):
- saveRequestWithoutMessage() and finalizeRequest() are now synchronized
  (same pattern as TransferIssueController.settle()) so concurrent saves
  from one session serialize; the second save sees the assigned id and
  edits instead of creating a duplicate
- resyncPharmaceuticalBillItemIfEmpty() runs for every line in
  saveBillComponent()/finalizeBillComponent(): a live line whose PBI has
  zero qty and free qty while its finance details hold a real quantity is
  rebuilt via calculateLineValues(), healing already-corrupted lines at
  the next save or finalize

Page (pharmacy_purhcase_order_request.xhtml), same guard pattern as
direct purchase (#21415):
- Qty, Free Qty and P Price inputs in the items table swallow Enter and
  blur instead, committing the blur-AJAX recalculation rather than firing
  a full-page Save mid-edit
- Supplier and item autocompletes swallow Enter unless the suggestion
  panel is open, so PrimeFaces handles only the selection

Closes #21417

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: adea69a3-088b-4c91-98d1-4b42826c66b4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 21417-po-pbi-guard-hotfix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

buddhika75 added a commit that referenced this pull request Jun 11, 2026
…zero-qty PBIs

A double-submit race on the purchase order request page (Enter-key
defaultCommand firing btnSave while another save was in flight) persisted
the same in-memory BillItem twice, creating duplicate rows that share one
BillItemFinanceDetails with only one of them owning the
PharmaceuticalBillItem. Removing the visible duplicate could retire the
row holding the real PBI; the survivor then got a lazily created all-zero
PBI, and PO approval/GRN (which reads pbi.qty) showed qty 0 and blocked
the GRN.

Server side (PurchaseOrderRequestController):
- saveRequestWithoutMessage() and finalizeRequest() are now synchronized
  (same pattern as TransferIssueController.settle()) so concurrent saves
  from one session serialize; the second save sees the assigned id and
  edits instead of creating a duplicate
- resyncPharmaceuticalBillItemIfEmpty() runs for every line in
  saveBillComponent()/finalizeBillComponent(): a live line whose PBI has
  zero qty and free qty while its finance details hold a real quantity is
  rebuilt via calculateLineValues(), healing already-corrupted lines at
  the next save or finalize

Page (pharmacy_purhcase_order_request.xhtml), same guard pattern as
direct purchase (#21415):
- Qty, Free Qty and P Price inputs in the items table swallow Enter and
  blur instead, committing the blur-AJAX recalculation rather than firing
  a full-page Save mid-edit
- Supplier and item autocompletes swallow Enter unless the suggestion
  panel is open, so PrimeFaces handles only the selection

Cherry-picked from b544eb2 (development, #21420), same fix applied to
coop-prod via #21422.

Closes #21417

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buddhika75 and others added 17 commits June 12, 2026 02:53
…port

The Disposal Consumption Report mixed two sign conventions that coexist on
disposal-issue bills: `billItem.qty` is stored positive on both issue and
return bills, while `billItemFinanceDetails.valueAt*Rate` (and the bill-level
`billFinanceDetails.total*Value`) use a stock-direction sign — negative when
stock leaves, positive when it comes back. See
`DataAdministrationController.isFinanceValueNegative` for the canonical rule.

The byBill, byBillItem, and summary aggregations summed those fields raw, so:
- quantity columns counted issued and returned units both positive, inflating
  totals (e.g. 8 issued + 8 returned showed as 16 instead of 0);
- purchase/cost/retail summary columns netted to ~0 because issue and return
  rows cancelled, even when net consumption was non-zero, while the netTotal
  column (already signed correctly) was the only column that looked right.

Translate each row to consumption direction (issue +, return/cancel −) before
aggregating: negate the stored valueAt*Rate (its sign is opposite to
consumption) and negate qty on return/cancel rows. netTotal already carries
the right sign and is left alone. The byBill and byBillItem table now expose
the per-row consumption-direction values on PharmacyRow so totals and rows
agree, and the PDF export reads the same fields.

Closes #21025

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tock

Root cause of the Ruhunu phantom-receive corruption (#21266 / RC2,
12 bills, 75 lines): generateBillComponent() clones each issue line
including pbi.stock — the ISSUING department's stock row. Lines skipped
in the settle loop (zero qty or insufficient porter stock) stayed in
the in-memory billItems list, and the later edit(receivedBill) merge
cascade-INSERTed them still bound to the sender's stock with no
StockHistory: recorded as received, no stock moved.

- generateBillComponent(): clear the inherited stock binding; a receive
  line is bound only to the receiving dept's stock after addToStock()
- settle()/settleApprove(): all-or-nothing porter-stock pre-check
  (aggregated per batch, read fresh from DB) with explicit per-item
  errors — no silent skipping or zeroing mid-settle
- doSettle(): drop unpersisted (skipped) lines before the final merges
  so CascadeType.ALL can never insert them
- settleApprove(): on deduct failure, unbind the inherited stock and
  zero quantities (covers PRE bills saved before this fix)
- receive pages: expiry read from pbi.itemBatch (stock may be unbound
  until settle)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eturns

Root cause of the #21266 RC5 orphan-return corruption (Ruhunu bills
4336218 / 4337297, 2026-03-25): the returns were settled successfully
(bill numbers consumed, items + StockHistory written), then the user
clicked Close on a STALE "returns to approve" list row.
closeDisposalReturn() validated isCompleted() on the stale entity
(false) and merged the whole pre-settle image over the live row,
reverting completed/completedAt/deptId/insId while items and stock
movements remained - leaving stock inflated with no completed bill.
Both bills carry the BILLCLOSED=1 fingerprint; deptId sequence gaps
(000017/000020) prove the settles ran.

- DisposalReturnWorkflowController.closeDisposalReturn(): reload the
  bill with findWithoutCache (L2 bypass - another server may write
  under dual-version operation), validate and edit the CURRENT row,
  never the stale list entity
- IssueReturnController: guard save/finalize/settle with
  disposalReturnAlreadyProcessed() - fresh-state check refusing
  already completed/closed/cancelled bills; also blocks double-click
  re-settles that would duplicate items and stock movements

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…General Stores login

Root cause: the ruhunu MySQL database is shared between the rh (development)
app and rh-production. The rh app carries newer Privileges enum values that
rh-production did not have. Privileges for user "buddhika" in General Stores
(dept 9096) and Andrology lab (dept 11108) were saved from the rh app,
writing those unknown names into WEBUSERPRIVILEGE.

When rh-production queries WebUserPrivilege for a user/department, EclipseLink
tries to deserialize @Enumerated(EnumType.STRING) values such as
"AdminInactivePatients" that did not exist in the enum. This throws
EnumConstantNotPresentException, which is silently swallowed by
AbstractFacade.findByJpql (catch-all returns empty list). The empty list meant
every hasPrivilege() call returned false → no navigation menu items were shown
after logging in to General Stores.

Added 36 constants (grouped by feature area) copied verbatim from the rh
codebase so their labels and names are identical:
- Inward: InwardFormTemplateAdmin, InwardFormFill, InwardDocumentUpload,
  InpatientClinicalDischarge (also added to getCategory "Inward" case)
- Pharmacy workflow: PharmacyDischargeMedicineIssue,
  PharmacyIssueForRequest{Save,Finalize,Approve},
  PharmacyReceive{Finalize,Approve},
  PharmacyDisposalIssue{Finalize,Approve},
  PharmacyDirectPurchase{Save,Finalize,Approve},
  ArchiveOldStockHistory, ArchiveOldItemBatch
- Cashier/petty cash: PettyCashEditFinancialYear, PettyCashCancellationApproval,
  CashierHandoverStatusReport, SettleHandoverProofMissing,
  SettleNonCashPayments, ViewAllShiftShortageBills
- Clinical: ClinicalPatientBlacklist, ClinicalPatientPseudonymise
- Admin: AdminPatientRelationships, AdminInactivePatients, MergePatients
- Fund transfer: RequestFundTransfer, IssueFundTransfer, ReceiveFundTransfer,
  DeclineFundTransfer, ProcessFundTransferRequest, CancelOwnFundTransfer,
  CancelOthersFundTransfer, ViewFundTransferReports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or requests

Root cause of the #21266 RC6 qty mismatch (pbi 4912145, 2026-05-20:
stock moved 2 while the line said 1): the user-edited quantity lives in
BillItemFinanceDetails.quantity, but settle() moved stock using pbi.qty
- still holding the REQUESTED qty when a UI edit had not synced it.
updateBillItemRateAndValue() then rewrote pbi.qty from bifd AFTER the
stock ops, so the persisted line disagreed with the actual movement.
Fingerprint: bifd.QUANTITYBYUNITS=-1 (recomputed) vs
VALUEATPURCHASERATE=990=2x495 (generation-time, never recomputed).

- settle(): syncStockQtyFromUserEnteredQty() normalizes pbi.qty /
  pbi.qtyPacks / billItem.qty from bifd.quantity BEFORE any stock
  validation or movement - the qty that moves stock is always the qty
  that gets persisted. Also makes a zeroed line truly move nothing.
- updateBillItemRateAndValue(): recompute valueAtPurchase/Retail/Cost
  rate fields from the user-entered qty (were frozen at the requested
  qty from item generation).

QA reproduced the defect and verified the fix (edit issuing qty before
settling; stock movement, line qty and bifd values all agree).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eturn bills

Bill.billItems is mapped orphanRemoval=true + CascadeType.ALL, and
getBillItems() initialises a null collection to an empty ArrayList. Once
anything touches the getter in-session, the next billFacade.edit(bill)
merge treats the empty list as authoritative and issues DELETEs for all
of the bill's BILLITEM rows - blocked by the PHARMACEUTICALBILLITEM FK:
SQLIntegrityConstraintViolationException on finalize/settle.

Guard all 8 bill-edit call sites in IssueReturnController with
setBillItems(null) before the merge - a null (uninitialised) collection
is skipped by the merge entirely. Established codebase pattern
(StoreIssueController, OpticianSaleController, IntrimPrintController).
Bill items are managed independently via billItemFacade.

Detail: tmp/disposal-return-orphan-removal-fix.md (rh checkout).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a disposal return is already approved, closed, or the user lacks
the ApproveDisposalReturn privilege, every action button on
pharmacy_bill_return_issue.xhtml silently disappeared with no
explanation of why.

- Add status panels stating the exact reason: already approved (with
  date/user), closed (create a new return), or finalized-awaiting-
  approval - including an explicit notice when the logged-in user lacks
  the approve privilege
- Hide Save/Finalize/Approve on closed returns (controller guard
  already refused the write; the buttons should not invite the click)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ities

#21266 RC4 follow-up. When stock was insufficient at approval,
validateStockAvailability() MUTATED the return quantities down to the
available stock as a validation side effect: the first Approve click
failed with a message but silently reduced the quantities, and a second
click approved a return that no longer matched what was finalized.

- validateStockAvailability()/validateAllItemsStockAvailability(): new
  allowAutoCorrect parameter; the draft-stage auto-correct UX is kept,
  but approval validates WITHOUT mutating quantities
- approve(): new approvalQuantitiesMatchFinalized() guard - every
  item's session quantities (paid + free) are compared against the
  finalized quantities read fresh from the DB (findWithoutCache); any
  mismatch blocks approval with a per-item message and the return must
  be corrected and re-finalized

QA verified: failed approve no longer reduces quantities; re-click
refuses; approver-side edits refused; normal finalize-approve unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed quantities

Same defect and fix as GRN returns (9372143) - the controller is a
clone. When stock was insufficient at approval,
validateStockAvailability() mutated the return quantities down to the
available stock; a second Approve click then approved a return that no
longer matched what was finalized.

- allowAutoCorrect parameter: draft-stage auto-correct kept, approval
  validates without mutating
- approve(): approvalQuantitiesMatchFinalized() guard - per-item
  comparison against the finalized quantities read fresh from the DB;
  mismatch blocks approval and requires re-finalization

QA verified on pharmacy_direct_purchase_return_form.xhtml.
Refs #21266 RC4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…21266)

PharmacyStockAdjustmentBill bills (PHARMACY_STOCK_ADJUSTMENT_BILL) move
stock but were missing from calculateStockAdjustment()'s BillType list,
so every stock-take day showed a COGS variance equal to the adjustment
value (e.g. General Stores 2026-04-02: +2,913,812). Verified against all
21 such bills: pbi.qty x itemBatch purchase rate matches the StockHistory
delta exactly, and the existing qty sign split routes them into the
Issue/Receive rows correctly. Development branch already had this type
in the list; this aligns the hotfix line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt (#21266)

Cancelling a GRN Return caused one of the stock data errors under
remediation. The button is commented out (not removed) with an
explanatory note so it can be reactivated once all stock discrepancy
issues are settled. The other two reprint pages already had their
Cancel buttons commented out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cancelling a Transfer Receive bill or a cashier sale bill causes the
Cost of Goods Sold report to show incorrect figures after the
cancellation. Both buttons are commented out (not removed) with an
explanatory note so they can be reactivated once the COGS variance
is fixed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mp departmentType

Two bugs fixed in GrnReturnWorkflowController:

1. updateStock() now returns boolean. When deductFromStock() returns false
   (insufficient stock at approval time), the method restores the positive qty,
   shows a clear error to the user, and returns false. approve() rolls back the
   completed/approved flags on the bill and aborts — no silent success toast.
   Previously the qty was silently zeroed, no StockHistory was created, and the
   approval appeared to succeed (confirmed on ruhunu bill R/GRN/RHD/LBK/26/000029:
   stock dropped from 7 to 3 between creation and approval due to concurrent returns).

2. Both bill-creation paths now call setDepartmentType() from the session
   department, so bill.DEPARTMENTTYPE is no longer NULL on GRN Return bills.
   A null DEPARTMENTTYPE caused the bill to be excluded from department-type-
   filtered Stock Ledger views.

Closes #20616
Closes #20615
…ent partial commits

Before deducting any stock, validate ALL items against fresh DB stock in a
read-only pass. If any stock record is insufficient, return false immediately
with no mutations — eliminating the partial-commit problem raised in PR review.

The deduction loop is kept as a second-level guard for the narrow window where
a concurrent transaction drains stock between the pre-check and the deduction.

Closes #20616

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…a11y labels

Detach billItems before persisting the received bill so the cascade does
not re-insert them, eliminating the duplicate BillItem/PBI/BIFD rows seen
on Pharmacy Transfer Receive bills (Ruhunu prod). Also add a confirmation
guard on the Receive button.

Add aria labels / label `for` associations on the transfer receive and
cost of goods sold pages so automated (Playwright) accessibility lookups
can target the controls reliably.

Closes #21266

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Re-add edit(receivedBill) after fillData() so the recalculated bill and
  BillFinanceDetails totals persist (Codex P1). Safe now that BillItems are
  created with IDs before this merge, so no duplicate re-insert.
- Restore in-memory billItems in a finally block in saveBill() so the
  @SessionScoped bean keeps the receive rows if create() throws (CodeRabbit).
- Add a server-side `settling` re-entrancy guard around settle() to block
  duplicate processing from rapid double-click on the non-AJAX Receive button
  (CodeRabbit). Avoided the this.disabled=true client trick (project
  anti-pattern: disabled fields drop from POST).
- Add for=phmDept on the Department label in cost_of_goods_sold.xhtml for
  proper label/input association (CodeRabbit, accessibility).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unts (port PR #21348)

Cherry-pick of development PR #21348 (commits 9f898ba, 0966693)
onto ruhunu-prod hotfix. Refs #21266.

- All 12 COGS movement-row queries now filter by
  GREATEST(createdAt, completedAt, checkeAt) — the actual stock-movement
  (approval) date — instead of createdAt, aligning movements with
  StockHistory and removing spurious day-level variances
  (e.g. Main Lab 22 Mar 2026: -2,047,485.98 -> 0.00).
- GRN/Direct-Purchase CANCELLED types removed from GRN cash/credit row
  (already counted in Purchase Return row — was double-counted).
- Staff payment method excluded from "Sale" row (owned by "Sale Credit").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@buddhika75 buddhika75 changed the title hotfix(pharmacy): prevent duplicate PO request bill items and self-heal zero-qty PBIs (coop) hotfix(pharmacy): coop — PO request PBI fixes + stock-integrity fixes from #21401 Jun 11, 2026
…ults

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant