Stock Level Monitoring Applet
Overview
Stock Level Monitoring watches the quantity on hand at each location against an optimal level and a maximum level that you set per item category, and e-mails a list of people when anything falls below or rises above them. It is a pure alerting applet: it reads stock balances and writes nothing back to inventory, creates no documents and posts no journals.
Think of it as the warning half of Stock Replenishment. The two applets share the same template → event → run shape and the same Rules Logic / Negation Logic controls (this applet’s own source says the controls were ported from there). Replenishment acts — it raises purchase orders and transfers. Monitoring only tells you.
Where it fits
| Upstream | This applet | Downstream |
|---|---|---|
| Item categories (inventory labels) and the items carrying them, from Item Maintenance | Template — which categories to watch, at which optimal and maximum levels, and the e-mail subject and body to send | — |
| Companies and locations from the Organisation Applet | Template and run scope | — |
| Entities with e-mail addresses (customers, suppliers, contacts) | The recipient list | The alert e-mail |
bl_inv_current_location_stock_balance — the per-location balance the Stock Balance Applet reports on | Run — reads qty_ledger per item per location and records breaches | Alert e-mail, and the run’s own property_json |
| — | Event — a start date and, optionally, a template | One queued run |
Screens and menus
| Menu | Route | What it lists | Actions |
|---|---|---|---|
| Level Monitoring Runs | stock-level-monitoring | bl_inv_stock_level_monitoring_run_hdr — Run Name, Description, Created Date, Updated Date, Status | Open a run; Run Processor (re-run it now); Save; Delete |
| Level Monitoring Events | stock-level-monitoring-events | bl_inv_stock_level_monitoring_event_hdr — Event Name, Event Code, Created Date, Updated Date, Status | Create, open, delete |
| Level Monitoring Template | stock-level-monitoring-template | bl_inv_stock_level_monitoring_template_hdr — Template Code, Template Name, Created Date, Updated Date, Status | Create, open, delete |
| Settings | settings/field-settings, settings/default-selection, settings/release-notes, plus the shared Webhook, Feature Visibility and permission screens | See Configuration | — |
| Personalization | personalization/personal-default-selection, personalization/sidebar | Per-user defaults, sidebar | — |
The Template and Run screens each open into a view with four sub-sections: Locations, Companies, Categories, and (on a run) Emails. The Event view has Locations, Companies, Categories and Entities. Categories is the one that carries numbers — one row per item category, with an editable Optimal Stock Value and Maximum Stock Value.
Both grids label those two columns "(MYR)". They are not money: the backend compares them against bl_inv_current_location_stock_balance.qty_ledger, a quantity. Read them as units.
Listings load a fixed window and extend it with a More action rather than paging server-side, and the menu file records that Release Notes was routed but unreachable until its entry was added — both are visible in menu-items.ts and the listing components.
Screenshots needed
No screenshots exist for this applet. A capture session should take: the Template listing; a Template view showing the Categories grid with Optimal and Maximum values and the Negation Logic control; the Default Optimal / Maximum Stock Value pair with the inline warning callout; an Event view with Recurring ticked and the recurrence editor open; the Run view with the Run Processor button; and the run’s Emails section. A scratch tenant is required — the Emails section shows real entity e-mail addresses.
Configuration
Before you can use it
| Prerequisite | Where | Why |
|---|---|---|
| Item categories with items attached | Item Maintenance | A run resolves each category label to its item set; a category with no items contributes nothing. |
| Locations on the template | Organisation Applet | detectBreaches returns immediately when the run has no locations. Nothing is reported and nothing fails. |
| At least one optimal or maximum value | The template’s Categories grid | A category link with both values null is skipped. |
| Entities with an e-mail address linked to the run | The run’s Emails section | Recipients come from the run’s entity links; an entity with a blank email is dropped. With no recipient left the alert is skipped and only a warning is logged. |
| The job processor enabled for the tenant | BigLedger support | Queue code STOCK_LEVEL_MONITORING_RUN_PROCESSOR. Without it, events and manual runs queue and never execute. |
| Server-side permissions | Settings → Permission Set / Role Permission | See below. |
Applet settings
Settings are applet-local, and there are effectively none.
- Settings → Field Settings is the unbound eight-toggle stub — Unit Discount, SST/VAT/GST, WHT, Blanket Order, Segment, G/L Dimension, Profit Center, Project — with no form group, no
formControlNameand a SAVE button with no click handler. The labels belong to a sales-document settings screen and have nothing to do with stock monitoring. No exposed control found (routes and settings components checked at commit12cab54f). - Settings → Default Selection and Personalization → Default Selection offer Default Branch and Default Location, and do persist them (
saveMasterSettingsInit/savePersonalSettingsInit, with a RESET that writes nulls). ButDEFAULT_BRANCHandDEFAULT_LOCATIONare declared inapplet-settings.model.tsand read nowhere else in the applet — no screen pre-selects anything from them. They are declared, rendered and persisted, but not consumed, so nothing changes when you set them. - Settings → Release Notes shows the applet’s release notes.
Everything that actually changes behaviour lives on the template, the event and the run, not in Settings. That is the part to configure:
| Where | Setting | What it controls | Default |
|---|---|---|---|
| Template → Categories | Optimal Stock Value per category | A location’s qty_ledger below this is an UNDERSTOCK breach | empty (no understock check for that category) |
| Template → Categories | Maximum Stock Value per category | A location’s qty_ledger above this is an OVERSTOCK breach | empty (no overstock check) |
| Template → Categories | Default Optimal Stock Value, Default Maximum Stock Value | Applied to every category not listed, and only when Negation Logic is ENABLED | empty |
| Template / Event / Run | Negation Logic (category_filter_negation) | DISABLED = monitor the categories you picked. ENABLED = monitor every other active category in the same lists, using the two default values above | DISABLED |
| Template / Event / Run | Rules Logic (category_filter_logic, AND / OR) | Nothing. The value is carried from template to event to run and logged, but every picked category is always evaluated independently — OR semantics. AND is not implemented. | AND (and treated as OR) |
| Template | Email Subject, Email Body | Copied onto the run when the run is created, so editing the template later does not rewrite an alert that already went out | empty (a default subject is used) |
| Event | Cycle Start Date | A start date in the past or now queues the run RUN_NOW; a future date queues it for that moment | now |
| Run | Run Processor | Re-queues this run RUN_NOW | — |
Feature visibility / permissions
bl_applet_client_side_perm_dfn holds zero rows for stockLevelMonitoringApplet (checked 2026-09-14), and the applet checks no HIDE_* / SHOW_* keys, so the Feature Visibility screen has nothing to gate. Access is entirely server-side, through TntErpPermissions:
| Screen | Permission |
|---|---|
| Level Monitoring Template | API_STOCK_LEVEL_MONITORING_TEMPLATE_HDR_READ / _CREATE / _UPDATE / _DELETE |
| Level Monitoring Events | API_STOCK_LEVEL_MONITORING_EVENT_HDR_READ / _CREATE / _UPDATE / _DELETE |
| Level Monitoring Runs | API_STOCK_LEVEL_MONITORING_RUN_READ / _CREATE / _UPDATE / _DELETE |
| Run Processor button | API_STOCK_LEVEL_MONITORING_RUN_READ — the manual-run endpoint is guarded by the read permission, not by update or create |
Fields
Template → Create
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Template Code | Short code | Yes | Validators.required |
| Template Name | Display name | Yes | Validators.required |
| Description | Free text | No | |
| Email Subject | Subject line of the breach alert | No | Falls back to a built-in subject |
| Email Body | HTML body of the alert | No | Snapshotted onto each run at creation |
| Status | ACTIVE / … | No | Defaults to ACTIVE |
The template view adds Creation Date, Created By, Modified Date and Modified By as read-only fields, plus the Locations, Companies, Entities and Categories sections.
Event → Create
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Event Code | Short code | Yes | |
| Event Name | Display name | Yes | |
| Level Monitoring Template | The template the run will use | No validator, but required in practice | A run built from an event with no template is abandoned by the processor — see Lifecycle |
| Cycle Start Date | When the run should execute | No | Empty means now |
| Cycle End Date | Informational | No | Not read by the processor |
| Recurring + recurrence rule | An RRULE built by the recurrence editor | No | Stored on the event and never read — see Lifecycle |
| Description, Status | Free text; ACTIVE by default | No |
Run → view shows Run Name, Description and Status, the Locations, Companies and Categories snapshots, and the Emails list (Entity Name, Email, Updated Date, Actions).
Lifecycle and effects
This applet writes only its own three header tables and their link tables. No stock movement, no journal, no document. Quantity and amount signums do not apply — there is no generic document and no data-consistency object of the financial kind; the DCOs under StockLevelMonitoringDataConsistencyObjects/ validate the template, event and run rows themselves.
How a run is triggered. There are exactly two paths, and both insert into the STOCK_LEVEL_MONITORING_RUN_PROCESSOR queue:
- Creating an event.
StockLevelMonitoringEventHdrService.createinserts a queue event carrying the event GUID, withExecutionStrategy.RUN_NOWif the event’s start date is now or in the past andINSERT_TO_QUEUEif it is in the future. Updating an event does not re-queue anything. - Pressing Run Processor on a run (
GET …/stock-level-monitoring-run-hdrs/backoffice-ep/manual-run/{guid}), which queues the run GUIDRUN_NOW.
What the processor does. StockLevelMonitoringRunProcessor.processEvent branches on which GUID it was given:
- event GUID — loads the event; if the event has no
template_hdr_guidit logs an error and stops. Otherwise it creates a run header from the template and the event, then processes it. - run GUID with a template — processes that run.
- run GUID with no template — logs “Monitoring run without a template is not supported” and returns an empty result. A run created by hand and never linked to a template can never produce anything.
Processing a run (processRun):
- Snapshot the template’s locations, companies and entities onto the run, but only where the run has none yet — so a run that has been edited keeps its own scope.
- Snapshot the template’s category links onto the run the same way. The event’s own category links are deliberately not used: they hold category groups, not categories with stock values.
- Resolve Negation Logic from the run, falling back to the template.
- When negation is ENABLED, replace the picked categories with every other active category in the same label lists, each carrying the template’s default optimal and maximum values.
detectBreaches— for each category link with at least one of the two values set, for each item in that category, for each of the run’s locations, readbl_inv_current_location_stock_balance.qty_ledger(null treated as zero) and compare. Below optimal isUNDERSTOCK; above maximum isOVERSTOCK. Understock wins when both would apply, and the triple (item, location, breach type) is de-duplicated.- Write
breach_count,detected_atand the fullbreacheslist into the run header’sproperty_json. - Send the alert e-mail.
The alert e-mail. Recipients are the entities linked to the run, reduced to those with a non-blank email on bl_fi_mst_entity_hdr. Sender and subject come from the run header, falling back to the platform default sender and a built-in subject. The subject and body were copied from the template when the run was created, so changing the template does not rewrite an alert already sent. Every send is recorded in the e-mail history table. The e-mail service never throws — a failure is returned on the result, logged, and the run still completes.
Nothing displays the result. The breach list exists in the run header’s property_json and in the e-mail. The applet never reads property_json, breach_count or the e-mail history — there is no screen anywhere in it that shows what a run found.
Related applets
- Stock Replenishment Applet — the same template / event / run shape, and the same Rules Logic and Negation Logic controls, but it acts on the shortfall by creating purchase orders and transfers instead of e-mailing.
- Stock Balance Applet — reports on the same
qty_ledgerfigures this applet compares against. - Stock Availability Applet — the interactive way to answer “how much is at this location right now”.
- Item Maintenance — where the item categories this applet monitors are defined.
- Organisation Applet — the companies and locations a template can scope to.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A run completes and reports zero breaches although stock is clearly low | Most often the run has no locations — detectBreaches returns immediately when the location set is empty, without failing. | Check the run’s Locations section, and the template’s, and re-run. |
| Negation Logic is on and every run reports nothing | Neither Default Optimal Stock Value nor Default Maximum Stock Value is set, so every synthetic category link is skipped. The applet shows an inline warning and the backend logs one. | Set at least one of the two defaults on the template. |
| A category is listed on the template but never breaches | Its Optimal and Maximum values are both empty, so the link is skipped. | Enter at least one value on that row. |
| Breaches are detected but nobody gets an e-mail | No entity linked to the run has an e-mail address; the alert is skipped with a warning in the log. | Add entities with e-mail addresses to the run’s Emails section. |
| Editing the template’s Email Subject or Body does not change the alert | Subject and body are snapshotted onto the run when the run is created. | Edit them on the run, or create a new run. |
| Switching Rules Logic from OR to AND changes nothing | AND is not implemented; the value is propagated and logged, and every category is evaluated independently. | Narrow the category list instead. |
| An event was set to Recurring and only ran once | is_recurring and rrule are stored on the monitoring event and read by no backend service. The only queue insert happens once, at event creation. | Create one event per occurrence, or press Run Processor on the run. |
| An event was saved with the right dates but never ran | Only creating an event queues a run; updating one does not. | Delete and re-create the event, or run the run manually. |
| Runs sit and never execute | The STOCK_LEVEL_MONITORING_RUN_PROCESSOR job processor is not enabled for the tenant. | Ask support to enable it. |
| There is nowhere to see what the last run found | By design at this commit — the breach list only exists in the run’s property_json and the alert e-mail. | Read the e-mail; for the raw list, ask support to read the run header. |
| A user can press Run Processor although they only have read access | The manual-run endpoint is guarded by API_STOCK_LEVEL_MONITORING_RUN_READ. | Treat read on runs as “may re-run”; withhold it if that matters. |