Skip to main content

Pharmacy

What it does

Pharmacy is Zhenus Pharmacy & SCM. This release administers the product catalog and formulary on /pharmacy/catalog, stock levels / batch expiry on /pharmacy/stock, the append-only controlled-drug register on /pharmacy/controlled, procurement (suppliers, purchase orders, goods receipt, reorder) on /pharmacy/procurement, and dispensing on /pharmacy/dispense (till) and /patients/{id}/pharmacy (patient-record sale), and transfers and consumption on /pharmacy/transfers.

Two product kinds share one catalog:

  • Medical — clinical identity is a concept from the platform dictionary (RxNorm / clinical drug). Strength lives on that concept, not a typed field. Each row also has a SKU / trade name (Panadol and Emzor can share one concept) and a controlled schedule (UNSCHEDULED, I–V).
  • Non-medical — classified by a managed category (toiletries, devices, sundries). No concept is attached.

Both kinds can carry barcodes, registration numbers (NAFDAC, NDC, DIN, MHRA PL, ARTG, EMA, or a scheme you add), packs (carton → base unit), and an OTC list price.

The formulary is the per-tenant approved medical list. A product can exist as OTC stock without being on the formulary.

Stock tracks quantity by lot, expiry, and location. Status is a closed list (Available, Quarantined, Disposed) — never a typed string. Adjust, stock take, quarantine, and dispose each require a reason from a closed list. Expired and quarantined batches cannot leave Available; the server refuses outbound movements with HTTP 409.

The controlled register is an append-only log of controlled-drug movements. It is read-only in the UI. Rows appear as a side effect of stock actions; they cannot be edited or retired.

Procurement manages suppliers (a register, not a typed name on the order), purchase orders (Draft → Submitted → Partially received / Received, or Cancelled), goods receipt (the purchase stock-increase path), and a read-only reorder list for this facility. Goods receipt can resolve a product by barcode or registration number and captures lot, manufacture date, expiry, and unit cost. A supplier return decrements a received batch and requires a reason from a closed list.

Dispensing is one sale service with three intakes:

  1. Patient-record sale — open the chart, then Pharmacy sale. The patient is already in the URL. There is no search or picker. Fill an open medication order (prescription) or sell identified OTC.
  2. Pharmacy tillPharmacy → Dispensing. Search a patient by UHP ID or name (the only patient search in pharmacy), then the same sale. Chart / global search is not a second till.
  3. Anonymous walk-in — on the till only. No person is created. Controlled-schedule lines are refused (HTTP 409). Sign-off is not a bypass.

Identified controlled sales require witness and pharmacist sign-off. Expired batches are refused with HTTP 409 — there is no override. Returns restock or quarantine and need a reason from a closed list. Lot recall lists on-hand quantity and historical dispenses for a lot number.

Transfers move stock between facilities in two linked steps: create a draft (source is this facility), dispatch it into transit, then receive it at the destination. Status is a closed list (Draft, Dispatched, In transit, Received, Cancelled). Consumption decrements an available lot for lab reagent, ward, or encounter use. Reason is a closed list; notes are optional free text.

Where to find it

  • Pharmacy → Catalog/pharmacy/catalog
  • Pharmacy → Stock/pharmacy/stock
  • Pharmacy → Controlled register/pharmacy/controlled
  • Pharmacy → Procurement/pharmacy/procurement
  • Pharmacy → Dispensing/pharmacy/dispense (till)
  • Patient record → Pharmacy sale/patients/{id}/pharmacy (no picker)
  • Pharmacy → Transfers/pharmacy/transfers (Transfers and Consumption tabs)

You need pharmacy.catalog.read to see the catalog, pharmacy.stock.read for stock, pharmacy.controlled.read for the register, pharmacy.procurement.read for procurement, pharmacy.dispense.read for the till and dashboard sale, and pharmacy.transfer.read for transfers. Write on catalog, stock, procurement, dispense, or transfer uses the matching .write permission. The register has no write actions in the UI. M19-006 must map pharmacy.transferpharmacy.transfer.read in AuthorizationCatalog or the nav link disappears.

How to use it

Catalog

  1. Open Products. Add a medical product by searching the concept dictionary, or a non-medical product by choosing a category (register one first if the list is empty).
  2. Open Barcodes, registrations and packs on a row to map GTIN/EAN codes, file numbers, and pack factors. Do not put those identifiers on a concept.
  3. Use Scan at the top of the products tab to resolve a barcode or a registration number to exactly one product.
  4. Open Formulary to approve medical products for this organisation.
  5. Open Reference lists to manage categories and worldwide registration schemes.

Stock

  1. Open Pharmacy → Stock. Each row is a lot: SKU, dictionary concept (medical only), lot number, expiry, quantity, status, location.
  2. Use Status (Available / Quarantined / Disposed) and Near expiry (30 / 60 / 90 days) to filter. Both are selects, not typed fields.
  3. Adjust stock creates or changes a lot. Quantity is signed; Reason is required from the closed list. Optional notes are free text — they do not replace the reason.
  4. On a row: Stock take (reason is Counted), Quarantine, Dispose, or Movement trail (read-only history).
  5. If a lot is expired or quarantined, do not try to issue it. The server answers 409 and the message stays on the form.

Controlled register

  1. Open Pharmacy → Controlled register.
  2. Review the append-only log. There is no edit, retire, or void.

Procurement

  1. Open Pharmacy → Procurement. Tabs: Suppliers, Purchase orders, Goods receipt, Reorder.
  2. Suppliers — register a trading partner (code + name). Available to is facility-local or organisation-wide. Purchase orders pick from this list; do not type a supplier name.
  3. Purchase orders — create a draft against a supplier and catalog products (medical and non-medical). Search the catalog on each line; the picker shows formulary status (medical), controlled schedule, and receipt domain (Pharmacy SCM, Asset Management, or Information Technology) so you know where stock lands at goods receipt. Status is a select. Submit a draft, or cancel a draft / submitted order.
  4. Goods receipt — receive against a submitted or partially received PO. Scan a barcode or registration number, then enter quantity, lot, manufacture date, expiry, and unit cost. Each line shows the same classification badges (formulary, schedule, receipt domain). Return to supplier decrements a received batch; Reason is a select (Near expiry, Damaged, Recalled, Wrong item, Other).
  5. Reorder — read-only suggestions for this facility. Use Create PO on a row to raise an order; the list itself is not editable.

Dispensing

  1. Till — open Pharmacy → Dispensing. Tabs: New sale, History, Returns, Lot recall.
  2. Search a patient by UHP ID or name, or click Anonymous walk-in. Do not use chart search as a till.
  3. Add lines by catalog product, barcode, or registration number. The cart shows the FEFO lot and expiry. Quantity is in base units after pack convert.
  4. For an identified controlled line, enter Witnessed by and Pharmacist user id. Those fields are hidden on an anonymous sale — they are not a bypass.
  5. Complete sale. If a lot is expired, or an anonymous cart includes a controlled schedule, the server answers 409 and the message stays on the form. There is no override.
  6. History lists prior sales (sale kind is a select). Returns restock or quarantine a line; Reason is a select (Unclaimed, Patient return, Damaged, Wrong item, Other).
  7. Lot recall — enter a lot number to see on-hand quantity and historical dispenses.
  8. Dashboard — from the patient record, open Pharmacy sale. The patient is already known. There is no search and no anonymous action. Fill an open medication order or sell identified OTC with the same cart and sign-off rules.

Transfers and consumption

  1. Open Pharmacy → Transfers. Tabs: Transfers, Consumption.
  2. Create transfer — destination is a select of other facilities (this facility is the source and is not sent). Product and stock batch are catalog / lot picks. Medical lines show the dictionary concept.
  3. Status is a select (Draft, Dispatched, In transit, Received, Cancelled). Dispatch a draft, Receive a dispatched or in-transit transfer (confirm quantity per line), or Cancel before it is received.
  4. Consumption → Record consumption — pick a product and available lot, enter quantity, and choose a Reason (Lab reagent, Ward use, Encounter, Other). Encounter id and notes are optional. Notes do not replace the reason.

Automated journey

The Playwright suite src/test/e2e-fullstack/pharmacy-journey.spec.ts (FE-260, pairs backend M19-007) walks catalog → stock → procurement → till + dashboard → transfers → controlled register on the full-stack harness. Backend Milestone19PharmacyScmIntegrationTest owns the API journey.

What is not here yet

  • Auto-issuing purchase orders without an approval path

Permissions

pharmacy.catalog.read / .write gate the catalog. pharmacy.stock.read / .write gate stock. pharmacy.controlled.read gates the register (read-only). pharmacy.procurement.read / .write gate procurement. pharmacy.dispense.read / .write gate the till and dashboard sale. pharmacy.transfer.read / .write gate transfers and consumption.

Troubleshooting

SymptomLikely cause
The Pharmacy section is missing from the sidebarYou do not hold a pharmacy read permission, or the pharmacy module is not activated.
Cannot add a medical productThe concept picker is required. Strength is not a field you type.
Cannot add toothpasteRegister a product category first. Non-medical products must not have a concept.
Scan finds nothingThe barcode or registration number is not mapped, or it was retired.
Adjust / dispose returns 409The lot is expired or quarantined and cannot leave Available. Quarantine or dispose it instead of issuing it.
Controlled register has no edit buttonIntended. The register is append-only.
Cannot create a purchase orderRegister a supplier first. Product is a catalog pick, not a typed drug name.
Goods receipt list is emptyOnly submitted or partially received POs can be received. Submit the draft first.
Reorder has no editIntended. Suggestions are read-only; use Create PO to act.
Anonymous sale returns 409 on a controlled drugIntended. Identify the patient and complete sign-off, or remove the line. Sign-off is not a bypass.
Complete sale returns 409 on expiryThe lot is expired. There is no override. Quarantine or dispose it on Stock.
Pharmacy sale card missing on the patient recordYou need pharmacy.dispense.read, and M19-005 must map pharmacy.dispense in AuthorizationCatalog.
Dashboard has no patient searchIntended. The patient is the chart you opened. Use the till to search.
Transfers nav is missingYou need pharmacy.transfer.read, and M19-006 must map pharmacy.transfer in AuthorizationCatalog.
Destination list is emptyOnly other session facilities appear. This facility is the source and is omitted.
Receive is disabledOnly Dispatched or In transit transfers can be received. Dispatch the draft first.
Consumption returns 409The lot is expired or quarantined. There is no override.