Improve Traceability for Stock Entries and Issues
Requirements Summary
Derived from SELVSUP-56 description and acceptance criteria:
Transaction capture: every manual Stock Issue and Stock Receive produces a system-generated traceable document
Document contents: transaction type, date and time, facility/warehouse, program, products with batch/quantity/expiry, user who performed the transaction (with signature)
Immediate print: PDF available right after submission
Post-transaction access: documents browsable, searchable, reprintable from a dedicated view; cross-referenced from the bin card
Audit: all actions logged
Constraints:
No regression on Requisition & Fulfillment workflows
Cancellation is out of scope (split into a separate ticket)
1. Document Number Generation
Reuse the existing documentNumber field - no schema migration needed.
Existing infrastructure:
stock_events.documentnumber(DB column)stock_card_line_items.documentnumber(DB column - auto-copied from the event during processing atStockCardLineItem.createLineItemFrom())StockEvent.documentNumber,StockEventDto.documentNumber,StockCardLineItem.documentNumber(Java)stockEventDto.json,lineItem.json(API schemas)
Generation - backend side:
The document number is generated on the backend during event processing. The format is sequential per facility, per year.
Format: {YEAR}-{MONTH}-{FACILITY_CODE}-{SEQ} where:
YEAR = current year
MONTH = current month
FACILITY_CODE = code of the facility performing the operation (resolved from
facilityIdvia referencedata)SEQ = sequential number, zero-padded (e.g., 0001, 0002), auto-incremented per facility + month + year combination
Example:
2026-05-FAC001-0001,2026-05-FAC001-0002
Sequence storage:
New table:
document_number_sequences(id, facility_id, year, month, last_sequence_number)Unique constraint on
(facility_id, year)On event creation, the backend atomically increments the sequence and assigns the next number
Concurrency: use
SELECT ... FOR UPDATEor database-level locking to prevent duplicate numbers under concurrent submissions
Triggering generation - via eventOrigin enum:
A new nullable enum eventOrigin on StockEvent explicitly marks where the event came from. This replaces the need for a separate generation-trigger flag and also solves the broader problem of distinguishing event sources across all consumers.
enum EventOrigin {
ISSUE, // stockmanagement UI, issue
RECEIVE // stockmanagement UI, receive
}The enum is intentionally minimal - only the two origins that this HLD needs to classify. Other stock-event sources (adjustments, PI, fulfillment, requisition, external integrations) are not populated for the time being; they remain NULL on the column.
Schema migration:
ALTER TABLE stockmanagement.stock_events ADD COLUMN event_origin VARCHAR(50) NULL;
CREATE INDEX stock_events_facility_origin_processed_idx
ON stockmanagement.stock_events (facilityid, event_origin, processeddate DESC);The composite index matches the history view's query pattern (filter by facility + origin, sort by processed date). Supports both per-facility and per-origin filtering efficiently and scales as the table grows.
Rules:
StockEventProcessorgenerates a document number wheneventOrigin IS NOT NULL(i.e.,ISSUEorRECEIVE)The frontend sets
eventOriginonly for manual issue/receive submissionsFor the time being all the other callers (fulfillment POD/Shipment, requisition, adjustments UI, PI UI, external integrations, legacy data) leave
eventOriginas NULL and do not receive an auto-generated document number
The generated value propagates automatically to all stock_card_line_items via existing code at StockCardLineItem.java:176.
Sequence padding is going to be hardcoded to 4 digits (e.g. 0001). If a facility ever exceeds 9999 events in a year the number will grow to 5+ digits - no special handling needed.
Future consideration:
Because the document number is sequential per facility and tied to the originating event, issue and receive transactions between two facilities could later be linked via their document numbers - potentially enabling automatic prefilling of the receiving facility's form when a matching issue is submitted by the supplying facility.
The
eventOriginenum is extensible. New origins can be added without touching the sequence table, schema, or existing queries. If future work extends the design to other event sources, adding values to the enum and setting them on the relevant callers is straightforward.Fulfillment could reuse the
Order.orderCodeas its stock eventdocumentNumberas a follow-up inopenlmis-fulfillment. This would provide natural traceability from stock events back to the originating order without inventing a new identifier. Not part of this HLD.Requisition-driven stock events (submitted by
openlmis-requisitionviaStockEventStockManagementService) could later declare their origin via the enum. Not part of this HLD.Adjustments and Physical Inventory are deliberately not included today.
Backwards compatibility:
New
event_origincolumn is nullable. Existingstock_eventsrows get NULL; no UPDATE required during migration.Existing callers (fulfillment, external integrations, scripts) continue to submit DTOs without
eventOrigin; the field deserializes as NULL. No validation or processing breaks.Document number generation is gated by
eventOrigin IS NOT NULL, so legacy events (and any caller that doesn't set the field) never accidentally receive numbers unless configured otherwise.New Transaction History view filters on the same condition, so legacy and fulfillment-originated events are correctly excluded. They still appear on the bin card and contribute to SOH as before.
StockEventProcessor,StockCardService,CalculatedStockOnHandService, bin card, stock card summaries (V1, V2) andStockCardAggregate(requisition integration) do not readeventOrigin- untouched.
2. Signature Collection on Issue/Receive
Reuse the existing choose-date-modal from Physical Inventory:
On "Submit" click for issue or receive, show the modal collecting
occurredDateandsignatureThe modal already exists at
stock-choose-date-modal/Wire the collected
signatureinto theStockEventDtopayload (the field exists butadjustment-creation.service.jsnever sets it today)The value flows through to
StockEvent.signatureand gets copied to eachStockCardLineItem.signatureat persistence time
3. Print After Submission
Same pattern as Physical Inventory (physical-inventory-draft.controller.js:551-560):
After successful submission, show a confirmation modal: "Print this stock issue/receive?"
On confirm, open the PDF report URL in a new window
Backend - new Jasper report:
New report template for issue/receive documents (based on PI report)
New endpoint:
GET /api/stockEvents/{id}/printinReportsController. EnforceSTOCK_CARDS_VIEWexplicitly at the controller level by callingpermissionService.canViewStockCard(programId, facilityId)after resolving the event's program and facility - matching the pattern ingetStockCardSummaries()rather than the weaker pattern ingetStockCard().Report contents (from ticket acceptance criteria):
Transaction type (Issue / Receive)
Document number
Date and time
Facility name
Program name
User who performed the transaction + signature
Table of line items: product, lot code, expiry date, source/destination name, source/destination comments, reason, reason comments, quantity, stock on hand at time of operation
Data sources: StockEvent holds facility, program, user, date. Its StockEventLineItems hold product, lot, quantity, source/destination, reason. The StockCard linked from each line item holds orderable and lot references. Facility, program, orderable, lot, and node names are resolved from referencedata.
4. Transaction History View
New UI state: openlmis.stockmanagement.transactionHistory at route /stockmanagement/transactionHistory
Purpose: browse, filter, and reprint past issue/receive transactions. Scoped per facility + program - both selected on entry to the view (consistent with stock card summaries, PI, and requisition flows). Page header displays the current facility and program; rows do not repeat them.
Transaction list columns and their data sources:
Document number -
StockEvent.documentNumberTransaction type (Issue / Receive) - read directly from
StockEvent.eventOrigin(ISSUE-> Issue,RECEIVE-> Receive)Occurred date - from line items (
StockEventLineItem.occurredDate), all line items in one event share the same dateNumber of products - count of
StockEventLineItemrows for the eventUser -
StockEvent.userId, resolved to username via referencedata serviceActions - Print: always visible (requires
STOCK_CARDS_VIEW)
Only events where event_origin IS NOT NULL appear in this view. This excludes adjustments, physical inventories, fulfillment-originated events, legacy events (NULL origin), and any external integrations that haven't set an origin.
Expandable row / detail view - products within a transaction:
Each row is expandable (or clickable to a detail sub-view) showing the line items:
Product name + code
Lot code
Expiry date
Source / Destination name
Quantity
Reason
Stock on hand after operation
Permissions:
Viewing the history list and transaction details:
STOCK_CARDS_VIEW(existing right, scoped to program + facility)Printing a transaction report:
STOCK_CARDS_VIEW(same as viewing)
Filters:
Transaction type (Issue / Receive / All)
Date range
Document number search
Backend - new endpoint:
GET /api/stockEvents?facilityId=...&programId=...&type=issue|receivewith pagination - bothfacilityIdandprogramIdare requiredReturns
StockEventrecords whereevent_origin IN ('ISSUE', 'RECEIVE')- excludes adjustments, PI, fulfillment-originated events, and legacy events with NULL originIncludes line item details in the response (products, lots, quantities)
Permission:
STOCK_CARDS_VIEW(scoped to program + facility)Extension of existing
StockEventsControlleror a new controller
Stock card detail view (bin card) and its report - additions:
Add a
documentNumbercolumn to the existing bin card line items table (stock-card.html)Render the document number as a clickable link navigating to the Transaction History detail view only when the event's origin is
ISSUEorRECEIVE. Other line items (NULL origin - fulfillment, requisition, adjustments, PI, legacy data) have no document number to show, so the column is simply blank for them.Add the same
documentNumbercolumn to the stock card Jasper print report (GET /api/stockCards/{id}/printinReportsController) so printed bin cards also include the document reference
5. Audit Logging
Stock event creation: already logged (existing flow)
Existing application logging framework (SLF4J / Logback)
6. Summary of Changes
Backend (openlmis-stockmanagement):
Area | Change |
|---|---|
| New endpoint |
New Jasper template | Issue/receive report template (based on PI report) |
Existing stock card Jasper report | Add |
| New endpoint |
| New nullable |
| Generate document number when |
New table + service | Sequence storage and generation logic per facility + year |
Flyway migration |
|
Frontend (openlmis-stockmanagement-ui):
Area | Change |
|---|---|
| Set |
| Show date+signature modal before submit (reuse |
| Add |
New: | New view for browsing/filtering past issue/receive transactions with expandable product details and print action |
Database migration:
New
document_number_sequencestable for sequential numberingNew
event_origin VARCHAR(50) NULLcolumn onstock_events(no UPDATE of existing rows - they remain NULL)New composite index
(facility_id, event_origin, processed_date DESC)onstock_eventsfor history view query performance
7. Known Risks, Gaps, and Open Questions
choose-date-modalmessage keys (blocker, not optional) - the modal currently displays "Physical Inventory submitted by ${username}" (stockChooseDateModal.submittedBy). Reusing it for issue/receive requires renaming or generalizing the Transifex message keys and coordinating with the translation community (per OpenLMIS convention, key changes lose existing translations). This is a prerequisite for the signature modal reuse, not a nice-to-have.Event ID availability after submission - the print flow requires the saved event ID to build the print URL (
GET /api/stockEvents/{id}/print). The currentStockEventsController.createStockEvent()returns the event ID in the response. The frontend submission flow (StockEventRepository.create()) needs to capture and pass this ID to the print confirmation modal.Signature is optional - the
choose-date-modaldoes not enforce signature input. If the user leaves it blank, the transaction is submitted without user identification beyonduserId. To be decided whether to make signature mandatory for issue/receive or accept it as optional.Legacy events cannot be retroactively classified - existing
stock_eventsrows getevent_origin = NULLon migration. They are correctly excluded from the new history view but cannot be back-classified asISSUE/RECEIVE. Acceptable since they never had document numbers to display anyway.Existing
GET /api/stockCards/{id}/printhas no controller-level permission check -ReportsController.getStockCard()relies onStockCardService.findStockCardById()for permission enforcement, which is bypassed whenauth.isClientOnly() == trueor when the facility matches the user's home. Not introduced by this HLD but worth flagging as a latent gap. The newGET /api/stockEvents/{id}/printendpoint in section 3 does an explicit controller-level check (getStockCardSummaries()pattern) and does not inherit the gap.
8. User Stories
Story 1: Stock Receive with document generation and print
A storekeeper at a district warehouse receives a delivery of EPI vaccines from the provincial warehouse.
The storekeeper navigates to Stock Management > Receive and selects the EPI program
They add line items: BCG Vaccine (Lot B1, qty 500, expiry 2027-03), Measles Vaccine (Lot M3, qty 200, expiry 2026-11)
They select the source facility (Provincial Warehouse) from the dropdown and a reason for each line
They click Submit
A modal appears asking for the occurred date and signature (storekeeper types their name)
On confirmation, the system submits the event. The backend generates document number
2026-DWH01-0001A second modal appears: "Print this stock receive?" with Print / No buttons
The storekeeper clicks Print - a PDF opens in a new tab showing:
Header: "Stock Receive - 2026-DWH01-0001"
Facility name, program, date, user + signature
Table: BCG Vaccine | Lot B1 | 2027-03 | Provincial Warehouse | 500 | SOH: 1200
Table: Measles Vaccine | Lot M3 | 2026-11 | Provincial Warehouse | 200 | SOH: 850
The storekeeper prints the document and files it
Story 2: Stock Issue with document generation and print
The same storekeeper issues vaccines to a health facility.
They navigate to Stock Management > Issue and select the EPI program
They add line items: BCG Vaccine (Lot B2, qty 100, expiry 2026-08)
They select the destination facility (Health Center Maputo) and a reason
They click Submit
Modal collects occurred date and signature
The system submits the event. The backend generates document number
2026-DWH01-0002Print modal appears, storekeeper clicks Print
PDF shows: "Stock Issue - 2026-DWH01-0002" with all transaction details
The document is handed to the driver as proof of dispatch
Story 3: Browsing transaction history
A supervisor visits the warehouse to review recent stock movements.
They navigate to Stock Management > Transaction History
They see a table listing recent issue/receive transactions:
Document Number | Type | Date | Products | User | Actions |
|---|---|---|---|---|---|
2026-DWH01-0001 | Receive | 2026-04-15 | 2 | J. Silva | Print / View |
2026-DWH01-0002 | Issue | 2026-04-15 | 1 | J. Silva | Print / View |
They use the date range filter to narrow to the last 7 days
They click on
2026-DWH01-0001to expand - the product details appear (BCG Vaccine, Measles Vaccine with lots, quantities, sources)They click Print on the issue row to reprint the document for the driver who lost the original
Story 4: Viewing document number on the bin card
A pharmacist wants to trace where a specific batch of vaccine came from.
They navigate to a stock card for BCG Vaccine at their facility
The bin card table now includes a Document Number column:
Date | From | To | Reason | Qty | SOH | User | Signature | Doc Number |
|---|---|---|---|---|---|---|---|---|
2026-04-15 | Provincial WH |
| Received | 500 | 1200 | J. Silva | J. Silva | 2026-DWH01-0001 |
2026-04-10 |
|
| Damaged - write-off | 20 |