Inventory Management
Model inventory visibility, locations, material types, stock movements, batch tracking, expiry, and replenishment levels.
Exampleinventorymanagementrequirementscrudapirestdatabaseexpressionsui
๐ Horizon
Inventory Management at a Glance
Business Need
The source specification contains 16 user requirements for inventory visibility and control. The system must distinguish stock statuses and material types, show stock across locations, support material movements, and retain batch, expiry, quantity, and replenishment data.
User Requirements Specification
The table below is the source specification for this example. M means mandatory and D means desirable. The URS IDs are referenced throughout Voyage so you can see how each requirement influences the model.
| URS ID | Requirement | Type |
|---|---|---|
| 1 | Display inventory in statuses such as available, quarantine, restricted, quality hold, in transit, rejected, lost, damaged, blocked, and expired. | M |
| 2 | Show inventory levels and statuses across the network, including central and local depots, sites, and external organizations. | M |
| 3 | Show inventory for raw, semi-finished, intermediate, finished, packaging, and labelling materials. | M |
| 4 | Generate inventory reports by storage location. | M |
| 5 | Manage material movements, including issuing, receiving, storage, dispensing, and returns. | M |
| 6 | Track the end-to-end status of materials through their stages. | M |
| 7 | Define multiple storage locations. | D |
| 8 | Receive and issue inventory into the desired location. | M |
| 9 | Perform periodic counting. | D |
| 10 | Perform cycle counting and produce cycle-count reports. | D |
| 11 | Differentiate available stock from quarantine, blocked, and returned stock. | M |
| 12 | Store minimum, replenishment, and maximum quantities by material and storage type. | M |
| 13 | Alert users about low inventory levels and upcoming expiry events. | D |
| 14 | Allow only available stock to be shipped or moved. | M |
| 15 | Show the status of a material or batch at a given location. | M |
| 16 | Manage expiry dates and batch numbers for every product or material. | M |
Architecture
Users manage inventory through an API or user interface. Expressions apply inventory rules, while the database preserves locations, stock, and history information.
What It Demonstrates
- Turning numbered requirements into traceable Ocean models.
- Sharing domain types across APIs, persistence, rules, and UI.
- Modeling CRUD, searches, stock operations, and reporting views.
- Separating the system model from its implementation choices.
Expected Result
An inventory application that can manage locations and inventory items, search stock by status, location, material type, batch, or expiry date, and perform move, receive, issue, and return operations.
Counting, automatic alerts, enforced shippability, and automatic history creation are modeled as foundations or extension points, but are not all active end-to-end in this version.
๐งญ Voyage
1. Start from the Requirements
You will use the URS as the starting point and gradually turn its business language into Ocean DSL. Mandatory requirements are marked M; desirable requirements are marked D.
The model will grow in the following order:
The example uses Gin for the REST API, PostgreSQL for persistence, HTMX and Bootstrap for the UI, and a Docker-oriented PostgreSQL service in deployment. These are realization choices introduced in Voyage; the requirements and Horizon remain technology-independent.
2. Model the Inventory Domain
Represent the allowed stock statuses
URS 1 and 11 list specific states that users must be able to distinguish. Because those values are fixed, you will model them as an enum rather than unrestricted text.
@datatype
enum ItemStatus
available
inQuarantine
restricted
qualityHold
inTransit
rejected
lost
damaged
blocked
expired
Represent the supported material types
URS 3 gives another fixed vocabulary. MaterialType ensures
that inventory searches, forms, and stored items all use the same six
categories.
enum MaterialType
rawMaterial
semiFinished
intermediate
finishedProduct
packagingMaterial
labellingMaterial
Define a storage location
URS 2 and 7 require inventory across multiple sites and storage locations. A location therefore has its own generated ID, name, site, and warehouse.
Location
id pattern LOC-UUID
name: String
site: String
warehouse: String
Define an inventory item
InvItem brings the core requirements together. It records
material identity and location, expected and counted quantities,
replenishment levels, counting information, alerts, batch number, and
expiry date.
InvItem
id pattern II-UUID
batchId: String
name: String
materialType: MaterialType
location: Location
status: ItemStatus
expectedQuantity: Int
countedQuantity: Int
lastCountingDate: DateTime
minLevel: Int
maxLevel: Int
replenishmentQuantity: Int
alertsEnabled: Boolean
expiryDate: DateTime
This type gives URS 12 and 16 a concrete home and provides the data needed for counting and alert extensions in URS 9, 10, and 13.
Describe stock operations
Moving, receiving, issuing, and returning stock need different inputs.
Small input types make each operation explicit without overloading
InvItem itself.
MoveItemInput
itemId: String
targetLocationId: String
quantity: Int
ReceiveItemInput
itemId: String
quantity: Int
IssueItemInput
itemId: String
quantity: Int
reason: String
code: String
ReturnItemInput
itemId: String
quantity: Int
reason: String
Prepare history and counting records
URS 6, 9, and 10 need traceability and counting information. The model defines event types, history records, count inputs, and count summaries so those capabilities can be connected incrementally.
enum ItemEventType
created
received
issued
returned
moved
counted
statusChanged
updated
deleted
ItemHistoryRecord
id pattern HIST-UUID
itemId: String
timestamp: DateTime
eventType: ItemEventType
fromLocationId: String
toLocationId: String
quantity: Int
comment: String
Afterwards, the domain vocabulary will be ready for API and persistence modeling.
3. Define the Inventory API
Create the REST API
You will expose the inventory capabilities through
InvMgtApi. Its metadata selects Gin, connects an API
configuration type, uses /inventory as the common path, and
enables Swagger generation.
@api
InvMgtApi style: rest
engine = gin
configType = ApiConfig
version = 1.0.0
description = Inventory Management API
basePath = /inventory
generateSwagger = true
Manage locations
URS 7 needs more than a location datatype: users must be able to create, find, update, and remove locations. Searches by site and warehouse also support network-wide visibility from URS 2.
get /location/{id} getLocation(_) : Location
get /locations/{offset}/{limit} listLocations(offset: Int, limit: Int) : List<Location>
get /locations/site/{site} findLocationBySite(site: String) : List<Location>
get /locations/warehouse/{warehouse} findLocationByWarehouse(warehouse: String) : List<Location>
post /location createLocation(loc: Location) : Location
put /location updateLocation(loc: Location) : Location
delete /location/{id} deleteLocation(_) : _
Manage inventory items
The next endpoints provide the basic CRUD contract for inventory. The
same InvItem datatype is used as input, output, and later as
the stored entity.
get /item/{id} getInvItem(_) : InvItem
get /items/{offset}/{limit} listInvItems(offset: Int, limit: Int) : List<InvItem>
post /item createInvItem(item: InvItem) : InvItem
put /item updateInvItem(item: InvItem) : InvItem
delete /item/{id} deleteInvItem(_) : _
Search inventory from the URS
URS 1โ4, 15, and 16 become typed searches. Users will be able to filter by status, location, material type, expiry date, or batch, and combine a location with a material name or batch ID.
get /items/status/{status} findInvItemsByStatus(status: ItemStatus) : List<InvItem>
get /items/location/{locationId} findInvItemsByLocation(locationId: String) : List<InvItem>
get /items/material-type/{materialType} findInvItemsByMaterialType(materialType: MaterialType) : List<InvItem>
get /items/expiring-before/{expiryDate} findItemsExpiringBefore(expiryDate: DateTime) : List<InvItem>
get /items/batch/{batchId} findItemsByBatchId(batchId: String) : List<InvItem>
Expose stock movements
URS 5 and 8 describe business actions rather than generic CRUD. Each action gets a dedicated endpoint and the matching input type defined on the previous page.
post /item/move moveItem(input: MoveItemInput) : InvItem
post /item/receive receiveItem(input: ReceiveItemInput) : InvItem
post /item/issue issueItem(input: IssueItemInput) : InvItem
post /item/return returnItem(input: ReturnItemInput) : InvItem
Expose history
The API also provides history lookups for URS 6. These endpoints can return all records, records for one item, or one event type.
get /item/{id}/history getItemHistory(id: String) : List<ItemHistoryRecord>
get /item/{id}/history/type/{eventType} getItemHistoryByType(id: String, eventType: ItemEventType) : List<ItemHistoryRecord>
get /items/history/{offset}/{limit} listAllItemHistory(offset: Int, limit: Int) : List<ItemHistoryRecord>
Keep counting as the next increment
Counting endpoints already exist as commented designs. They make the intended URS 9 and 10 contract visible, but they will not be generated until the comments are removed and the remaining flow is completed.
#post /item/count countItem(input: CountItemInput) : InvItem
#get /item/{id}/count-history listItemCountHistory(id: String) : List<CountItemInput>
#get /items/count-summary getCountSummary(_) : List<CountSummary>
4. Preserve Inventory Data
Declare the database
You will use PostgreSQL for this realization. The database declaration selects its engine and the configuration type that will provide the connection details.
@database
Database InvDb
engine = postgres
configType = InvMgtDatabaseConfig
tags = primary
Store inventory and connect it to locations
InvItem becomes an entity keyed by ID. Indexes support the
main URS searches, and the many-to-one relationship connects many stock
items to one location.
Entity InvItem
key(id)
indexes: index(status), index(locationId), index(batchId), index(expiryDate)
location -m2o-> Location
Add inventory queries and commands
Queries mirror the API searches, while transactional commands create, update, and delete inventory items.
query findByStatus(status: ItemStatus) : List<InvItem>
query findByLocationId(locationId: String) : List<InvItem>
query findByMaterialType(materialType: MaterialType) : List<InvItem>
query findByBatchId(batchId: String) : List<InvItem>
query findByExpiryDateBefore(expiryDate: DateTime) : List<InvItem>
command T createInvItem(item: InvItem) : InvItem
command T updateInvItem(item: InvItem) : InvItem
command deleteInvItem(id: String) : _
Store locations
Location becomes a second entity. Its unique name and site
and warehouse indexes support location management and network searches.
Entity Location
key(id)
indexes: unique(name), index(site), index(warehouse)
query findByName(name: String): List<Location>
query findBySite(site: String): List<Location>
query findByWarehouse(warehouse: String): List<Location>
command T createLocation(item: Location) : Location
command T updateLocation(item: Location) : Location
command deleteLocation(id: String) : _
Store history records
The third entity prepares end-to-end traceability. It can store events for each item and query them by item, type, or time.
Entity ItemHistoryRecord
key(id)
indexes: index(itemId), index(eventType), index(timestamp)
query findByItemId(itemId: String) : List<ItemHistoryRecord>
query findByItemIdAndEventType(itemId: String, eventType: ItemEventType) : List<ItemHistoryRecord>
command T createItemHistoryRecord(record: ItemHistoryRecord) : ItemHistoryRecord
Afterwards, the example will have persistent operations for stock, locations, and history. Expressions can now coordinate those operations into inventory behavior.
5. Express Inventory Rules
Capture reusable requirement checks
Several URS statements translate naturally into small Boolean expressions. These helpers describe low stock, shippable status, approaching expiry, count eligibility, and quantity discrepancy.
IsBelowMinLevel
input: item: InvItem
output: result: Boolean
logic
result = item.countedQuantity < item.minLevel
IsShippable
input: item: InvItem
output: result: Boolean
logic
result = item.status == ItemStatus.available
IsExpiringBefore
input: item: InvItem & refDate: DateTime
output: result: Boolean
logic
result = item.expiryDate <= refDate
These expressions model parts of URS 13 and 14. In this version they are not yet connected to automatic alerts or the issue operation, so they are foundations rather than enforced end-to-end behavior.
Move stock between locations
A movement first validates the quantity, subtracts it from the source, and creates a destination item with the moved quantity and shared batch and material details.
MoveItemExpression
input: movementItem: MoveItemInput
output: updatedItem: InvItem
logic
src = InvItem.findById(movementItem.itemId)
if movementItem.quantity <= 0 then
error "quantity must be a positive number"
end
if movementItem.quantity > src.countedQuantity then
error "quantity exceeds available stock"
end
src.countedQuantity = src.countedQuantity - movementItem.quantity
InvItem.updateInvItem(src)
# A destination InvItem is then created for targetLocationId.
Receive, issue, and return quantities
The remaining stock operations follow the same teaching pattern: load the item, validate a positive quantity, update physical and expected quantities where appropriate, then persist the result.
IssueItemExpression
input: issueItem: IssueItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(issueItem.itemId)
if issueItem.quantity > invItem.countedQuantity then
error "not enough physical stock available"
end
invItem.countedQuantity = invItem.countedQuantity - issueItem.quantity
invItem.expectedQuantity = invItem.expectedQuantity - issueItem.quantity
InvItem.updateInvItem(invItem)
Read inventory history
History expressions wrap the generated queries and provide methods the service can connect to API operations.
GetItemHistoryExpression
input: itemId: String
output: history: List<ItemHistoryRecord>
logic
history = ItemHistoryRecord.findByItemId(itemId)
The current movement expressions do not yet create
ItemHistoryRecord entries. Adding those writes is the next
step needed for complete URS 6 traceability.
Prepare cycle counting
A counting expression already updates the physical quantity and last count date. It remains disconnected while the commented API and service definitions are completed.
CountItemExpression
input: countInput: CountItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(countInput.itemId)
invItem.countedQuantity = countInput.countedQuantity
invItem.lastCountingDate = countInput.lastCountingDate
InvItem.updateInvItem(invItem)
6. Configure the Components
Collect service configuration
The inventory service will need API, database, and logging settings. You will group them into one configuration contract.
@config
@import config O.log.config@1.0.0 as LogConfig
InvMgtConfig
apiConfig : ApiConfig
dbConfig : InvMgtDatabaseConfig
logConfig : LogConfig
Configure API and UI ports
Small configuration types make both exposed components portable across environments while keeping useful local defaults.
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig : LogConfig
Configure PostgreSQL
Database settings include the connection URL, credentials, SSL choice, and retry behavior used while the runtime starts.
InvMgtDatabaseConfig
url: String (default=jdbc:postgresql://localhost:5432)
user: String (default=inv_mgt_user)
pass: String (default=inv_mgt_pass)
sslMode: Boolean (default=false)
retriesNo: Int (default=10)
retriesDelaySec: Int (default=30)
7. Wire the Inventory Service
Assemble the service
InvMgtService will combine the configuration, API,
database, and stock-operation expressions into one backend boundary.
@service
InvMgtService
use config InvMgtConfig as myCfg
impl api InvMgtApi as api on myCfg.apiConfig.port
use database InvDb as invDb
use expression MoveItemExpression
use expression ReceiveItemExpression
use expression IssueItemExpression
use expression ReturnItemExpression
Connect straightforward CRUD and searches
Operations that need no extra business rule can connect directly to generated entity queries and commands.
connect api.getLocation -> Location.findById
connect api.createLocation -> Location.createLocation
connect api.getInvItem -> InvItem.findById
connect api.findInvItemsByStatus -> InvItem.findByStatus
connect api.findInvItemsByLocation -> InvItem.findByLocationId
connect api.findInvItemsByMaterialType -> InvItem.findByMaterialType
connect api.createInvItem -> InvItem.createInvItem
Connect stock operations through expressions
Movement endpoints need validation and multiple persistence steps, so they are routed through the expressions defined earlier.
connect api.moveItem -> MoveItemExpression
connect api.receiveItem -> ReceiveItemExpression
connect api.issueItem -> IssueItemExpression
connect api.returnItem -> ReturnItemExpression
Connect history reads
History endpoints are routed through their query expressions. Counting connections remain commented until that extension is completed.
connect api.getItemHistory -> GetItemHistoryExpression
connect api.getItemHistoryByType -> GetItemHistoryByTypeExpression
connect api.listAllItemHistory -> ListAllItemHistoryExpression
#connect api.countItem -> CountItemExpression
Afterwards, API calls will have a clear route either directly to persistence or through inventory business logic.
8. Build Inventory Dashboards and UI
Organize the main workflows
The example provides separate dashboards for inventory, locations, history, and application information. The inventory dashboard combines a table with forms for searching, CRUD, and stock movements.
@dashboard
InventoryDashboard
title: ๐ฆ Inventory
layout: SingleColumnLayout
Widget InvTable of type Table
title = Inventory Items
limit = 5
Describe a movement form
Dashboard fields reuse the operation input types conceptually. For a movement, users provide an item, target location, and quantity.
Widget MoveInvItemForm of type Form
title = Move Inventory Item
fields:
- itemId String
- targetLocationId String
- quantity Int
buttons:
submit : ๐ Move Inventory Item
Select the UI technology
The UI realization uses HTMX, Bootstrap, Go HTML templates, and a Gin
backend. It receives its port and logging settings through
UiConfig.
@ui
InvMgtUi
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
configType: UiConfig
Add dashboard navigation
Each major workflow becomes a navigation entry, giving users focused spaces for stock, history, and location management.
nav Inventory dashboard = InventoryDashboard
nav History dashboard = HistoryDashboard
nav Location dashboard = LocationDashboard
nav About dashboard = AboutDashboard
Connect UI actions to the API
Tables and forms reuse the API contract. The UI therefore adds no second set of inventory operations.
connect inv.InvTable.listRows -> InvMgtApi.listInvItems
connect inv.MoveInvItemForm.submit -> InvMgtApi.moveItem
connect inv.ReceiveInvItemForm.submit -> InvMgtApi.receiveItem
connect inv.IssueInvItemForm.submit -> InvMgtApi.issueItem
connect inv.ReturnInvItemForm.submit -> InvMgtApi.returnItem
connect his.HisTable.listRows -> InvMgtApi.listAllItemHistory
9. Define Deployment
Provide the database runtime
You will import Ocean's packaged PostgreSQL service and expose it as the inventory database runtime.
@deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
InvMgtDatabase
service PostgresqlDB
Deploy the inventory service
The backend runs with two replicas, exports its API on port 9092, and waits for the database dependency.
InvMgtDeploy
service InvMgtService
replica 2
export 9092:InvMgtService.api
dependsOn InvMgtDatabase
Deploy the UI
The UI runs separately on port 8082 and depends on the inventory backend.
InvMgtUiDeploy
service InvMgtUi
replica 1
export 8082:InvMgtUi.config
dependsOn InvMgtDeploy
Afterwards, the concrete topology will be ready: UI โ inventory service โ database.
10. Validate and Explore
After reviewing the model, you can add the example to your workspace and explore the generated application.
- Open the
Inventory Managementexample in Ocean-lab. - Validate the complete model and inspect any reported references.
- Generate the API, persistence, UI, and deployment artifacts.
- Follow the generated
Readme.mdto start the system. - Open the API documentation at
localhost:9092/swagger/index.html. - Open the user interface at
localhost:8082. - Create locations, then create inventory items assigned to them.
- Search by status, material type, location, batch, and expiry.
- Try receive, issue, return, and move operations.
11. Conclusion and Next Steps
By the end of this example, you will have seen how numbered user requirements can become one connected Ocean model. Afterwards, you should understand how to:
- translate requirement vocabularies into datatypes and enums;
- turn business searches and actions into typed API operations;
- design related entities, indexes, queries, and commands;
- use expressions for multi-step inventory operations;
- connect backend capabilities to dashboards and deployment;
- identify the difference between modeled and fully wired behavior.
Useful next steps are to create history records during every stock
operation, activate cycle counting and count summaries, connect low
stock and expiry alerts, and call IsShippable before issuing
or moving stock. Those changes would close the remaining URS gaps
without changing the foundation of the model.
Executable model
<\> Implementation
Explore the runnable model by responsibility, then select a file to inspect its complete source.
# @ocean-meta-start
# tags:
# - documentation
# perspective:
# feature: inventory-management
# @ocean-meta-end
@info
name: Inventory Management
version: 1.0.0
description: The Inventory Management application enables complete visibility and control of materials across the supply network, supporting multiple inventory statuses such as unrestricted, quarantine, blocked, and in-transit.<br><br>It provides multi-location management, material movements, periodic and cycle counting, batch and expiry tracking, threshold-based alerts, and reporting by storage location and material type.
# @ocean-meta-start
# tags:
# - inventory-management
# - datatype
# perspective:
# feature: inventory-management
# @ocean-meta-end
@datatype
enum ItemStatus
available
inQuarantine
restricted
qualityHold
inTransit
rejected
lost
damaged
blocked
expired
enum MaterialType
rawMaterial
semiFinished
intermediate
finishedProduct
packagingMaterial
labellingMaterial
InvItem
id pattern II-UUID
batchId: String
name: String
materialType: MaterialType
location: Location
status: ItemStatus
expectedQuantity: Int
countedQuantity: Int
lastCountingDate: DateTime
minLevel: Int
maxLevel: Int
replenishmentQuantity: Int
alertsEnabled: Boolean
expiryDate: DateTime
Location
id pattern LOC-UUID
name: String
site: String
warehouse: String
CountItemInput
itemId: String
countedQuantity: Int
lastCountingDate: DateTime
MoveItemInput
itemId: String
targetLocationId: String
quantity: Int
ReceiveItemInput
itemId: String
quantity: Int
ReturnItemInput
itemId: String
quantity: Int
reason: String
IssueItemInput
itemId: String
quantity: Int
reason: String
code: String
ItemHistoryRecord
id pattern HIST-UUID
itemId: String
timestamp: DateTime
eventType: ItemEventType
fromLocationId: String
toLocationId: String
fromStatus: ItemStatus
toStatus: ItemStatus
quantity: Int
comment: String
enum ItemEventType
created
received
issued
returned
moved
counted
statusChanged
updated
deleted
CountSummary
id pattern CNT-UUID
itemId: String
name: String
locationId: String
expectedQuantity: Int
countedQuantity: Int
lastCountingDate: DateTime
discrepancy: Int
# @ocean-meta-start
# tags:
# - deployment
# perspective:
# feature: inventory-management
# service: all
# @ocean-meta-end
@deploy
Name: InvMgt-deploy
@import service P.db.postgres.docker@1.0.0 as PostgresqlDB
InvMgtDeploy
service InvMgtService
replica 2
export 9092:InvMgtService.api
dependsOn InvMgtDatabase
InvMgtDatabase
service PostgresqlDB
InvMgtUiDeploy
service InvMgtUi
replica 1
export 8082:InvMgtUi.config
dependsOn InvMgtDeploy
# @ocean-meta-start
# tags:
# - database
# - postgres
# perspective:
# feature: inventory-management
# service: inv-mgt-service
# @ocean-meta-end
@database
Database InvDb
engine = postgres
configType = InvMgtDatabaseConfig
tags = primary
# ---------------------------------------------------
# InvItem Entity
# ---------------------------------------------------
Entity InvItem
key(id)
indexes: index(status), index(locationId), index(batchId), index(expiryDate)
# Relations
location -m2o-> Location
# Queries
query listInvItems(offset:Int, limit:Int) : List<InvItem>
query findById(id: String) : InvItem
query findByStatus(status: ItemStatus) : List<InvItem>
query findByLocationId(locationId: String) : List<InvItem>
query findByMaterialType(materialType: MaterialType) : List<InvItem>
query findByBatchId(batchId: String) : List<InvItem>
query findByExpiryDateBefore(expiryDate: DateTime) : List<InvItem>
query findByLocationAndName(locationId: String, name: String) : List<InvItem>
query findByLocationAndBatchId(locationId: String, batchId: String) : List<InvItem>
# Commands
command T createInvItem(item: InvItem) : InvItem
command T updateInvItem(item: InvItem) : InvItem
command deleteInvItem(id: String) : _
# ---------------------------------------------------
# Location Entity
# ---------------------------------------------------
Entity Location
key(id)
indexes: unique(name), index(site), index(warehouse)
# Queries
query listLocations(offset:Int, limit:Int) : List<Location>
query findById(id: String) : Location
query findByName(name: String): List<Location>
query findBySite(site: String): List<Location>
query findByWarehouse(warehouse: String): List<Location>
# Commands
command T createLocation(item: Location) : Location
command T updateLocation(item: Location) : Location
command deleteLocation(id: String) : _
# ---------------------------------------------------
# ItemHistoryRecord Entity
# ---------------------------------------------------
Entity ItemHistoryRecord
key(id)
indexes: index(itemId), index(eventType), index(timestamp)
# Queries
query findByItemId(itemId: String) : List<ItemHistoryRecord>
query findByItemIdAndEventType(itemId: String, eventType: ItemEventType) : List<ItemHistoryRecord>
query listAll(offset:Int, limit:Int) : List<ItemHistoryRecord>
# Commands
command T createItemHistoryRecord(record: ItemHistoryRecord) : ItemHistoryRecord
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: inventory-management
# target: generic check
# service: inv-mgt-service
# @ocean-meta-end
@expression
# Check if the item's stock is below minimum threshold
IsBelowMinLevel
input: item: InvItem
output: result: Boolean
logic
result = item.countedQuantity < item.minLevel
# Check if the item can be shipped (e.g., only from unrestricted/available stock)
IsShippable
input: item: InvItem
output: result: Boolean
logic
result = item.status == ItemStatus.available
# Check if the item is approaching its expiry date (based on a reference date)
IsExpiringBefore
input: item: InvItem & refDate: DateTime
output: result: Boolean
logic
result = item.expiryDate <= refDate
# Check if the item is currently available for counting (e.g., based on status)
IsCountable
input: item: InvItem
output: result: Boolean
logic
result = item.status != ItemStatus.blocked AND item.status != ItemStatus.restricted
# Check if a stock movement is allowed (status, location, and quantity > 0)
CanMoveItem
input: item: InvItem & targetLocation: Location & quantity: Int
output: result: Boolean
logic
result = quantity > 0 AND item.location.id != targetLocation.id
# Determine if item has any discrepancy between expected and counted
HasDiscrepancy
input: item: InvItem
output: result: Boolean
logic
result = item.countedQuantity != item.expectedQuantity
# @ocean-meta-start
# tags:
# - expression
# perspective:
# feature: inventory-management
# target: service specific
# service: inv-mgt-service
# @ocean-meta-end
@expression
# -------------------------
# Inventory Item Movement / Stock Operations
# -------------------------
MoveItemOldExpression
input: movementItem: MoveItemInput
output: updatedItem: InvItem
logic
invItem = InvItem.findById(movementItem.itemId)
invItem.locationId = movementItem.targetLocationId
InvItem.updateInvItem(invItem)
updatedItem = InvItem.findById(movementItem.itemId)
MoveItemExpression
input: movementItem: MoveItemInput
output: updatedItem: InvItem
logic
src = InvItem.findById(movementItem.itemId)
if movementItem.quantity <= 0 then
error "quantity must be a positive number"
end
if movementItem.quantity > src.countedQuantity then
error "quantity exceeds available stock"
end
src.countedQuantity = src.countedQuantity - movementItem.quantity
src.expectedQuantity = src.expectedQuantity - movementItem.quantity
InvItem.updateInvItem(src)
var dest InvItem
dest.batchId = src.batchId
dest.name = src.name
dest.materialType = src.materialType
dest.locationId = movementItem.targetLocationId
dest.status = src.status
dest.expectedQuantity = movementItem.quantity
dest.countedQuantity = 0
dest.minLevel = src.minLevel
dest.maxLevel = src.maxLevel
dest.replenishmentQuantity = src.replenishmentQuantity
dest.alertsEnabled = src.alertsEnabled
dest.expiryDate = src.expiryDate
InvItem.createInvItem(dest)
updatedItem = InvItem.findById(movementItem.itemId)
ReceiveItemExpression
input: receiveItem: ReceiveItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(receiveItem.itemId)
if receiveItem.quantity <= 0 then
error "quantity must be a positive number"
end
invItem.countedQuantity = invItem.countedQuantity + receiveItem.quantity
InvItem.updateInvItem(invItem)
invItem = InvItem.findById(receiveItem.itemId)
IssueItemExpression
input: issueItem: IssueItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(issueItem.itemId)
if issueItem.quantity <= 0 then
error "quantity must be a positive number"
end
if issueItem.quantity > invItem.countedQuantity then
error "not enough physical stock available"
end
invItem.countedQuantity = invItem.countedQuantity - issueItem.quantity
invItem.expectedQuantity = invItem.expectedQuantity - issueItem.quantity
InvItem.updateInvItem(invItem)
invItem = InvItem.findById(issueItem.itemId)
ReturnItemExpression
input: returnItem: ReturnItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(returnItem.itemId)
if returnItem.quantity <= 0 then
error "quantity must be a positive number"
end
invItem.countedQuantity = invItem.countedQuantity + returnItem.quantity
invItem.expectedQuantity = invItem.expectedQuantity + returnItem.quantity
InvItem.updateInvItem(invItem)
invItem = InvItem.findById(returnItem.itemId)
# -------------------------
# Inventory Item History
# -------------------------
GetItemHistoryExpression
input: itemId: String
output: history: List<ItemHistoryRecord>
logic
history = ItemHistoryRecord.findByItemId(itemId)
GetItemHistoryByTypeExpression
input: itemId: String & eventType: ItemEventType
output: history: List<ItemHistoryRecord>
logic
history = ItemHistoryRecord.findByItemIdAndEventType(itemId, eventType)
ListAllItemHistoryExpression
input: offset: Int & limit: Int
output: history: List<ItemHistoryRecord>
logic
history = ItemHistoryRecord.listAll(offset, limit)
# -------------------------
# Inventory Counting / Cycle Counting
# -------------------------
CountItemExpression
input: countInput: CountItemInput
output: invItem: InvItem
logic
invItem = InvItem.findById(countInput.itemId)
invItem.countedQuantity = countInput.countedQuantity
invItem.lastCountingDate = countInput.lastCountingDate
InvItem.updateInvItem(invItem)
ListItemCountHistoryExpression
input: itemId: String
output: history: List<ItemHistoryRecord>
logic
history = ItemHistoryRecord.findByItemId(itemId)
# @ocean-meta-start
# tags:
# - configuration
# perspective:
# feature: inventory-management
# service: all
# @ocean-meta-end
@config
@import config O.log.config@1.0.0 as LogConfig
InvMgtConfig
apiConfig : ApiConfig
dbConfig : InvMgtDatabaseConfig
logConfig : LogConfig
ApiConfig
port: Int (default=8080)
UiConfig
port: Int (default=8080)
logConfig : LogConfig
InvMgtDatabaseConfig
url: String (default=jdbc:postgresql://localhost:5432)
user: String (default=inv_mgt_user)
pass: String (default=inv_mgt_pass)
sslMode: Boolean (default=false)
retriesNo: Int (default=10)
retriesDelaySec: Int (default=30)
# @ocean-meta-start
# tags:
# - inv-mgt-api
# - rest-api
# perspective:
# feature: inventory-management
# service: inv-mgt-service
# @ocean-meta-end
@api
InvMgtApi style: rest
engine = gin
configType = ApiConfig
tags = external
version = 1.0.0
description = Inventory Management API
basePath = /inventory
generateSwagger = true
# -------------------------
# Location Endpoints
# -------------------------
get /location/{id} getLocation(_) : Location
get /locations/{offset}/{limit} listLocations(offset: Int, limit: Int) : List<Location>
get /locations/name/{name} findLocationByName(name: String) : List<Location>
get /locations/site/{site} findLocationBySite(site: String) : List<Location>
get /locations/warehouse/{warehouse} findLocationByWarehouse(warehouse: String) : List<Location>
post /location createLocation(loc: Location) : Location
put /location updateLocation(loc: Location) : Location
delete /location/{id} deleteLocation(_) : _
# -------------------------
# Inventory Item Endpoints
# -------------------------
get /item/{id} getInvItem(_) : InvItem
get /items/{offset}/{limit} listInvItems(offset: Int, limit: Int) : List<InvItem>
get /items/status/{status} findInvItemsByStatus(status: ItemStatus) : List<InvItem>
get /items/location/{locationId} findInvItemsByLocation(locationId: String) : List<InvItem>
get /items/material-type/{materialType} findInvItemsByMaterialType(materialType: MaterialType) : List<InvItem>
get /items/expiring-before/{expiryDate} findItemsExpiringBefore(expiryDate: DateTime) : List<InvItem>
get /items/batch/{batchId} findItemsByBatchId(batchId: String) : List<InvItem>
post /item createInvItem(item: InvItem) : InvItem
put /item updateInvItem(item: InvItem) : InvItem
delete /item/{id} deleteInvItem(_) : _
post /item/move moveItem(input: MoveItemInput) : InvItem
post /item/receive receiveItem(input: ReceiveItemInput) : InvItem
post /item/issue issueItem(input: IssueItemInput) : InvItem
post /item/return returnItem(input: ReturnItemInput) : InvItem
get /items/location/{locationId}/name/{name} findInvItemsByLocationAndName(locationId: String, name: String) : List<InvItem>
get /items/location/{locationId}/batch/{batchId} findInvItemsByLocationAndBatch(locationId: String, batchId: String) : List<InvItem>
# -------------------------
# Inventory Item History Endpoints
# -------------------------
get /item/{id}/history getItemHistory(id: String) : List<ItemHistoryRecord>
get /item/{id}/history/type/{eventType} getItemHistoryByType(id: String, eventType: ItemEventType) : List<ItemHistoryRecord>
get /items/history/{offset}/{limit} listAllItemHistory(offset: Int, limit: Int) : List<ItemHistoryRecord>
# -------------------------
# Counting / Cycle Count Endpoints
# -------------------------
#post /item/count countItem(input: CountItemInput) : InvItem
#get /item/{id}/count-history listItemCountHistory(id: String) : List<CountItemInput>
#get /items/count-summary getCountSummary(_) : List<CountSummary>
# @ocean-meta-start
# tags:
# - dashboard
# - ui
# perspective:
# service: ui-service
# @ocean-meta-end
@dashboard
InventoryDashboard
title: ๐ฆ Inventory
subtitle: Manage your inventory items!
layout: SingleColumnLayout
Widget InvTable of type Table
title = Inventory Items
limit = 5
columns:
- id String
- batchId String
- name String
- materialType MaterialType
- location Location
- status ItemStatus
- expectedQuantity Int
- countedQuantity Int
- lastCountingDate DateTime
- minLevel Int
- maxLevel Int
- replenishmentQuantity Int
- alertsEnabled Boolean
- expiryDate DateTime
Widget S50 of type Separator
size = lg
style = solid
label = MANAGE
Widget ManageInvItemText of type Text
subtitle = To manage the inventory items!
content = Here you can manage inventory items via <b>move</b>, <b>receive</b>, <b>issue</b>, or <b>return</b> actions.
Widget S51 of type Separator
size = md
style = solid
Widget MoveInvItemForm of type Form
title = Move Inventory Item
fields:
- itemId String
- targetLocationId String
- quantity Int
buttons:
submit : ๐ Move Inventory Item
Widget S52 of type Separator
size = md
style = solid
Widget ReceiveInvItemForm of type Form
title = Receive Inventory Item
fields:
- itemId String
- quantity Int
buttons:
submit : ๐ฅ Receive Inventory Item
Widget S53 of type Separator
size = md
style = solid
Widget IssueInvItemForm of type Form
title = Issue Inventory Item
fields:
- itemId String
- quantity Int
- reason String
- code String
buttons:
submit : โก๏ธ Issue Inventory Item
Widget S54 of type Separator
size = md
style = solid
Widget ReturnInvItemForm of type Form
title = Return Inventory Item
fields:
- itemId String
- quantity Int
- reason String
buttons:
submit : โฉ๏ธ Return Inventory Item
Widget S30 of type Separator
size = lg
style = solid
label = SEARCH
Widget SearchText of type Text
subtitle = To find the inventory items!
content = Here you can search based on <b>ID</b>, <b>Status</b>, <b>Location ID</b>, <b>Material Type</b>, or <b>Batch ID</b>.
Widget S31 of type Separator
size = md
style = solid
Widget FindInvByIdForm of type Form
title = Find Inventory Item By ID
fields:
- id String
buttons:
submit : ๐ Find by ID
Widget S32 of type Separator
size = md
style = solid
Widget FindInvByStatusForm of type Form
title = Find Inventory Item By Status
fields:
- status ItemStatus
buttons:
submit : ๐ Find by Status
Widget S33 of type Separator
size = md
style = solid
Widget FindInvByLocIdForm of type Form
title = Find Inventory Item By Location-ID
fields:
- locationId String
buttons:
submit : ๐ Find by Location-ID
Widget S34 of type Separator
size = md
style = solid
Widget FindInvByMatTypeForm of type Form
title = Find Inventory Item By Material Type
fields:
- materialType MaterialType
buttons:
submit : ๐ Find by Material Type
Widget S35 of type Separator
size = md
style = solid
Widget FindInvByExpiringDateForm of type Form
title = Find Inventory Item By Expiring Date
fields:
- expiryDate DateTime
buttons:
submit : ๐ Find by Expiring Date
Widget S36 of type Separator
size = md
style = solid
Widget FindInvByBatchIdForm of type Form
title = Find Inventory Item By Batch-ID
fields:
- batchId String
buttons:
submit : ๐ Find by Batch-ID
Widget S37 of type Separator
size = lg
style = solid
label = CRUD
Widget CrudInvText of type Text
subtitle = To Create/Update/Delete inventory items!
content = Here you can <b>Create</b>, <b>Update</b>, or <b>Delete</b> inventory items.
Widget S38 of type Separator
size = md
style = solid
label = CREATE
Widget CreateInvItemForm of type Form
title = Create New Inventory Item
fields:
- batchId String
- name String
- materialType MaterialType
- locationId String
- status ItemStatus
- expectedQuantity Int
- countedQuantity Int
- lastCountingDate DateTime
- minLevel Int
- maxLevel Int
- replenishmentQuantity Int
- alertsEnabled Boolean
- expiryDate DateTime
buttons:
submit : โ Create Inventory Item
Widget S39 of type Separator
size = md
style = solid
label = UPDATE
Widget UpdateInvItemForm of type Form
title = Update Inventory Item
fields:
- id String
- batchId String
- name String
- materialType MaterialType
- locationId String
- status ItemStatus
- expectedQuantity Int
- countedQuantity Int
- lastCountingDate DateTime
- minLevel Int
- maxLevel Int
- replenishmentQuantity Int
- alertsEnabled Boolean
- expiryDate DateTime
buttons:
submit : โ๏ธ Update Inventory Item
fetch : ๐ Fetch Item by ID
cancel : ๐งน Clear
Widget S40 of type Separator
size = md
style = solid
label = DELETE
Widget DeleteInvItemForm of type Form
title = Delete Inventory Item
fields:
- id String
buttons:
submit : โ Delete Inventory Item
LocationDashboard
title: ๐ Locations
subtitle: Manage all your inventory locations!
layout: SingleColumnLayout
Widget LocTable of type Table
title = Locations
limit = 5
columns:
- id String
- name String
- site String
- warehouse String
Widget S1 of type Separator
size = lg
style = solid
label = SEARCH
Widget SearchText of type Text
subtitle = To find the desired locations!
content = Here you can search based on <b>Name</b>, <b>Site</b>, or <b>Warehouse</b>.
Widget S2 of type Separator
size = md
style = solid
Widget FindLocByNameForm of type Form
title = Find Location By Name
fields:
- name String
buttons:
submit : ๐ Find by Name
Widget S2_1 of type Separator
size = md
style = solid
Widget FindLocationBySiteForm of type Form
title = Find Location By Site
fields:
- site String
buttons:
submit : ๐ Find by Site
Widget S3 of type Separator
size = md
style = solid
Widget FindLocationByWarehouseForm of type Form
title = Find Location By Warehouse
fields:
- warehouse String
buttons:
submit : ๐ Find by Warehouse
Widget S4 of type Separator
size = lg
style = solid
label = CRUD
Widget CrudLocationText of type Text
subtitle = To Create/Update/Delete your locations!
content = Here you can <b>Create</b>, <b>Update</b>, or <b>Delete</b> locations.
Widget S5 of type Separator
size = md
style = solid
label = CREATE
Widget CreateLocationForm of type Form
title = Create New Locations
fields:
- name String
- site String
- warehouse String
buttons:
submit : โ Create Location
Widget S6 of type Separator
size = md
style = solid
label = UPDATE
Widget UpdateLocationForm of type Form
title = Update Locations
fields:
- id String
- name String
- site String
- warehouse String
buttons:
submit : โ๏ธ Update Location
fetch : ๐ Fetch Location by ID
Widget S7 of type Separator
size = md
style = solid
label = DELETE
Widget DeleteLocationForm of type Form
title = Delete Locations
fields:
- id String
buttons:
submit : โ Delete Location
HistoryDashboard
title: ๐ History
subtitle: Inventory Items History
layout: SingleColumnLayout
Widget HisTable of type Table
title = History
limit = 5
columns:
- id String
- itemId String
- timestamp DateTime
- eventType ItemEventType
- fromLocationId String
- toLocationId String
- fromStatus ItemStatus
- toStatus ItemStatus
- quantity Int
- comment String
Widget S20 of type Separator
size = lg
style = solid
label = SEARCH
Widget SearchHistText of type Text
subtitle = To find inventory items history
content = Here you can search based on <b>ID</b> and/or <b>Event Type</b>.
Widget S21 of type Separator
size = md
style = solid
Widget FindHisByIdForm of type Form
title = Find History By ID
fields:
- id String
buttons:
submit : ๐ Find by ID
Widget S22 of type Separator
size = md
style = solid
Widget FindHisByIdAndEventTypeForm of type Form
title = Find History By Event Type
fields:
- id String
- eventType ItemEventType
buttons:
submit : ๐ Find by Event Type
AboutDashboard
title: โน๏ธ About
subtitle: App information!
layout: SingleColumnLayout
Widget AboutText of type Text
title = About Inventory Management App ๐ฆ
subtitle = version 1.0.0
content = The Inventory Management application enables complete visibility and control of materials across the supply network, supporting multiple inventory statuses such as unrestricted, quarantine, blocked, and in-transit.<br><br>It provides multi-location management, material movements, periodic and cycle counting, batch and expiry tracking, threshold-based alerts, and reporting by storage location and material type.
# @ocean-meta-start
# tags:
# - ui
# perspective:
# feature: inventory-management
# service: ui-service
# @ocean-meta-end
@ui
InvMgtUi
@perspectives: version:0.1.0, lifestyle:stable
framework: htmx
styling: bootstrap
template: go-html-template
backend: go-gin
static: static
configType: UiConfig
nav Inventory dashboard = InventoryDashboard
nav History dashboard = HistoryDashboard
nav Location dashboard = LocationDashboard
nav About dashboard = AboutDashboard
header title = ๐ฆ Inventory Management App!
header subtitle = Managing inventory made simple ๐
header align = center
footer title = โก Built with Ocean-lab
footer subtitle = Version 1.0.0
footer align = center
use dashboard InventoryDashboard as inv
use dashboard HistoryDashboard as his
use dashboard LocationDashboard as loc
use dashboard AboutDashboard as about
use api InvMgtApi
connect loc.LocTable.listRows -> InvMgtApi.listLocations
connect loc.FindLocByNameForm.submit -> InvMgtApi.findLocationByName
connect loc.FindLocationBySiteForm.submit -> InvMgtApi.findLocationBySite
connect loc.FindLocationByWarehouseForm.submit -> InvMgtApi.findLocationByWarehouse
connect loc.CreateLocationForm.submit -> InvMgtApi.createLocation
connect loc.UpdateLocationForm.submit -> InvMgtApi.updateLocation
connect loc.UpdateLocationForm.fetch -> InvMgtApi.getLocation
connect loc.DeleteLocationForm.submit -> InvMgtApi.deleteLocation
connect his.HisTable.listRows -> InvMgtApi.listAllItemHistory
connect inv.InvTable.listRows -> InvMgtApi.listInvItems
connect inv.MoveInvItemForm.submit -> InvMgtApi.moveItem
connect inv.ReceiveInvItemForm.submit -> InvMgtApi.receiveItem
connect inv.IssueInvItemForm.submit -> InvMgtApi.issueItem
connect inv.ReturnInvItemForm.submit -> InvMgtApi.returnItem
connect inv.FindInvByIdForm.submit -> InvMgtApi.getInvItem
connect inv.FindInvByStatusForm.submit -> InvMgtApi.findInvItemsByStatus
connect inv.FindInvByLocIdForm.submit -> InvMgtApi.findInvItemsByLocation
connect inv.FindInvByMatTypeForm.submit -> InvMgtApi.findInvItemsByMaterialType
connect inv.FindInvByExpiringDateForm.submit -> InvMgtApi.findItemsExpiringBefore
connect inv.FindInvByBatchIdForm.submit -> InvMgtApi.findItemsByBatchId
connect inv.CreateInvItemForm.submit -> InvMgtApi.createInvItem
connect inv.UpdateInvItemForm.submit -> InvMgtApi.updateInvItem
connect loc.UpdateInvItemForm.fetch -> InvMgtApi.getInvItem
connect inv.DeleteInvItemForm.submit -> InvMgtApi.deleteInvItem
# @ocean-meta-start
# tags:
# - coordinator
# - orchestrator
# - service
# perspective:
# feature: inventory-management
# service: inv-mgt-service
# @ocean-meta-end
@service
InvMgtService
@perspectives: version:0.1.0, lifestyle:stable
use config InvMgtConfig as myCfg
impl api InvMgtApi as api on myCfg.apiConfig.port
use database InvDb as invDb
# -------------------------
# Config binding
# -------------------------
connect myCfg.dbConfig -> invDb
# -------------------------
# Used Expressions
# -------------------------
use expression MoveItemExpression
use expression ReceiveItemExpression
use expression IssueItemExpression
use expression ReturnItemExpression
use expression GetItemHistoryExpression
use expression GetItemHistoryByTypeExpression
use expression ListAllItemHistoryExpression
#use expression CountItemExpression
#use expression ListItemCountHistoryExpression
# -------------------------
# Location Operations
# -------------------------
connect api.getLocation -> Location.findById
connect api.listLocations -> Location.listLocations
connect api.findLocationByName -> Location.findByName
connect api.findLocationBySite -> Location.findBySite
connect api.findLocationByWarehouse -> Location.findByWarehouse
connect api.createLocation -> Location.createLocation
connect api.updateLocation -> Location.updateLocation
connect api.deleteLocation -> Location.deleteLocation
# -------------------------
# Inventory Item Operations
# -------------------------
connect api.getInvItem -> InvItem.findById
connect api.listInvItems -> InvItem.listInvItems
connect api.findInvItemsByStatus -> InvItem.findByStatus
connect api.findInvItemsByLocation -> InvItem.findByLocationId
connect api.findInvItemsByMaterialType -> InvItem.findByMaterialType
connect api.findItemsExpiringBefore -> InvItem.findByExpiryDateBefore
connect api.findItemsByBatchId -> InvItem.findByBatchId
connect api.findInvItemsByLocationAndName -> InvItem.findByLocationAndName
connect api.findInvItemsByLocationAndBatch -> InvItem.findByLocationAndBatchId
connect api.createInvItem -> InvItem.createInvItem
connect api.updateInvItem -> InvItem.updateInvItem
connect api.deleteInvItem -> InvItem.deleteInvItem
# -------------------------
# Inventory Item Movement / Stock Operations
# -------------------------
connect api.moveItem -> MoveItemExpression
connect api.receiveItem -> ReceiveItemExpression
connect api.issueItem -> IssueItemExpression
connect api.returnItem -> ReturnItemExpression
# -------------------------
# Inventory Item History
# -------------------------
connect api.getItemHistory -> GetItemHistoryExpression
connect api.getItemHistoryByType -> GetItemHistoryByTypeExpression
connect api.listAllItemHistory -> ListAllItemHistoryExpression
# -------------------------
# Inventory Counting / Cycle Counting
# -------------------------
#connect api.countItem -> CountItemExpression
#connect api.listItemCountHistory -> ListItemCountHistoryExpression
#connect api.getCountSummary -> GetCountSummaryExpression
# URS Inventory System Requirements
| URS ID | Requirement Description | Requirement Type |
|------- |------------------------ |------------------|
| 1 | The system must have the ability to display stock/inventory in various statuses e.g. Unrestricted/available, In Quarantine, Restricted, On Quality Hold, In-Transit, Rejected, Lost, Damaged, Blocked and Expired. | M |
| 2 | The system must be able to show inventory levels and statuses across the network including CMOs (materials/IMP kits at central/local depots/clinical sites). | M |
| 3 | The system must be able to show inventory/stock for all material types: Raw Materials, semi-finished, intermediates, Finished Products, packaging, and labelling materials. | M |
| 4 | The system must be able to generate inventory report based on storage location. | M |
| 5 | The system must be able to perform material movements. The system should also be able to manage stock movements, including issuing, receiving, storage, dispensing, and returns. | M |
| 6 | The system must be able to track End-to-End status for the materials in various stages. | M |
| 7 | The system must be able to define multiple storage locations in the system. | D |
| 8 | The system must be able to receive and issue inventory into the desired location. | M |
| 9 | The system must be able to perform periodic counting. | D |
| 10 | The system must have the ability to perform cycle counting and generate the cycle counting reporting information. | D |
| 11 | The system must be able to differentiate unrestricted use stock from quarantine stock, blocked stock, and returns stock. | M |
| 12 | The system must be able to store a minimum quantity, replenishment quantity and maximum quantity for replenishment per material master data and storage type. | M |
| 13 | The system should provide users with alerts when inventory levels fall below certain thresholds and for upcoming expiry events. | D |
| 14 | The system should allow only unrestricted stock for shipping/movements. | M |
| 15 | The system should allow to see the status of a material and/or batch in the given location. | M |
| 16 | The system should also be able to manage expiry dates and batch numbers for each product/material in the inventory. | M |
flowchart
u[User]
subgraph sys[System]
subgraph s[InvMgtService]
a[API]
e[Expressions]
end
d[(Database)]
end
u -.communicates.-> a
a --> d
a -.-> e