Improve Traceability for Stock Entries and Issues

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 at StockCardLineItem.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 facilityId via 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 UPDATE or 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:

  • StockEventProcessor generates a document number when eventOrigin IS NOT NULL (i.e., ISSUE or RECEIVE)

  • The frontend sets eventOrigin only for manual issue/receive submissions

  • For the time being all the other callers (fulfillment POD/Shipment, requisition, adjustments UI, PI UI, external integrations, legacy data) leave eventOrigin as 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 eventOrigin enum 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.orderCode as its stock event documentNumber as a follow-up in openlmis-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-requisition via StockEventStockManagementService) 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_origin column is nullable. Existing stock_events rows 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) and StockCardAggregate (requisition integration) do not read eventOrigin - 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 occurredDate and signature

  • The modal already exists at stock-choose-date-modal/

  • Wire the collected signature into the StockEventDto payload (the field exists but adjustment-creation.service.js never sets it today)

  • The value flows through to StockEvent.signature and gets copied to each StockCardLineItem.signature at 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}/print in ReportsController. Enforce STOCK_CARDS_VIEW explicitly at the controller level by calling permissionService.canViewStockCard(programId, facilityId) after resolving the event's program and facility - matching the pattern in getStockCardSummaries() rather than the weaker pattern in getStockCard().

  • 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.documentNumber

  • Transaction 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 date

  • Number of products - count of StockEventLineItem rows for the event

  • User - StockEvent.userId, resolved to username via referencedata service

  • Actions - 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|receive with pagination - both facilityId and programId are required

  • Returns StockEvent records where event_origin IN ('ISSUE', 'RECEIVE') - excludes adjustments, PI, fulfillment-originated events, and legacy events with NULL origin

  • Includes line item details in the response (products, lots, quantities)

  • Permission: STOCK_CARDS_VIEW (scoped to program + facility)

  • Extension of existing StockEventsController or a new controller

Stock card detail view (bin card) and its report - additions:

  • Add a documentNumber column 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 ISSUE or RECEIVE. 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 documentNumber column to the stock card Jasper print report (GET /api/stockCards/{id}/print in ReportsController) 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

Area

Change

ReportsController

New endpoint GET /api/stockEvents/{id}/print returning PDF

New Jasper template

Issue/receive report template (based on PI report)

Existing stock card Jasper report

Add documentNumber column

StockEventsController or new controller

New endpoint GET /api/stockEvents with filtering by facility, program, type, including line item details

StockEvent domain + DTO

New nullable eventOrigin enum field

StockEventProcessor

Generate document number when eventOrigin IN (ISSUE, RECEIVE)

New table + service document_number_sequences

Sequence storage and generation logic per facility + year

Flyway migration

event_origin VARCHAR(50) NULL column on stock_events

Frontend (openlmis-stockmanagement-ui):

Area

Change

Area

Change

adjustment-creation.service.js

Set eventOrigin (ISSUE or RECEIVE based on the current flow), include signature in the event payload. userId is NOT set by the frontend - the backend derives it from the authenticated user (see StockEventProcessContextBuilder.java:141-145: for user-token requests the user comes from the auth principal, not the DTO).

adjustment-creation.controller.js

Show date+signature modal before submit (reuse choose-date-modal), show print confirmation after submit

stock-card.html

Add documentNumber column with clickable link to transaction history detail

New: stock-transaction-history/

New view for browsing/filtering past issue/receive transactions with expandable product details and print action

Database migration:

  • New document_number_sequences table for sequential numbering

  • New event_origin VARCHAR(50) NULL column on stock_events (no UPDATE of existing rows - they remain NULL)

  • New composite index (facility_id, event_origin, processed_date DESC) on stock_events for history view query performance


7. Known Risks, Gaps, and Open Questions

  1. choose-date-modal message 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.

  2. Event ID availability after submission - the print flow requires the saved event ID to build the print URL (GET /api/stockEvents/{id}/print). The current StockEventsController.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.

  3. Signature is optional - the choose-date-modal does not enforce signature input. If the user leaves it blank, the transaction is submitted without user identification beyond userId. To be decided whether to make signature mandatory for issue/receive or accept it as optional.

  4. Legacy events cannot be retroactively classified - existing stock_events rows get event_origin = NULL on migration. They are correctly excluded from the new history view but cannot be back-classified as ISSUE/RECEIVE. Acceptable since they never had document numbers to display anyway.

  5. Existing GET /api/stockCards/{id}/print has no controller-level permission check - ReportsController.getStockCard() relies on StockCardService.findStockCardById() for permission enforcement, which is bypassed when auth.isClientOnly() == true or when the facility matches the user's home. Not introduced by this HLD but worth flagging as a latent gap. The new GET /api/stockEvents/{id}/print endpoint 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.

  1. The storekeeper navigates to Stock Management > Receive and selects the EPI program

  2. They add line items: BCG Vaccine (Lot B1, qty 500, expiry 2027-03), Measles Vaccine (Lot M3, qty 200, expiry 2026-11)

  3. They select the source facility (Provincial Warehouse) from the dropdown and a reason for each line

  4. They click Submit

  5. A modal appears asking for the occurred date and signature (storekeeper types their name)

  6. On confirmation, the system submits the event. The backend generates document number 2026-DWH01-0001

  7. A second modal appears: "Print this stock receive?" with Print / No buttons

  8. 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

  9. 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.

  1. They navigate to Stock Management > Issue and select the EPI program

  2. They add line items: BCG Vaccine (Lot B2, qty 100, expiry 2026-08)

  3. They select the destination facility (Health Center Maputo) and a reason

  4. They click Submit

  5. Modal collects occurred date and signature

  6. The system submits the event. The backend generates document number 2026-DWH01-0002

  7. Print modal appears, storekeeper clicks Print

  8. PDF shows: "Stock Issue - 2026-DWH01-0002" with all transaction details

  9. 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.

  1. They navigate to Stock Management > Transaction History

  2. They see a table listing recent issue/receive transactions:

Document Number

Type

Date

Products

User

Actions

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

  1. They use the date range filter to narrow to the last 7 days

  2. They click on 2026-DWH01-0001 to expand - the product details appear (BCG Vaccine, Measles Vaccine with lots, quantities, sources)

  3. 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.

  1. They navigate to a stock card for BCG Vaccine at their facility

  2. The bin card table now includes a Document Number column:

Date

From

To

Reason

Qty

SOH

User

Signature

Doc Number

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

OpenLMIS: the global initiative for powerful LMIS software