What It Is
SIMS is the consumables half of UPLB’s property system. The Asset Management System tracks individual assets; SIMS tracks quantities — supplies moving in and out of supply offices, valued the way the Commission on Audit requires.
Everything runs off one posted stock ledger. Beginning Balances, Delivery Receipts, RIS, adjustments — they all post into it, and every number the app shows is derived from it. Nothing reports a figure just because a document claimed it.
Scope As It Stands
| Layer | What’s there |
|---|---|
| Master data | Items with SKUs and custom fields, brands, categories, PhilGEPS categories, units of measure, suppliers, types, an office tree, employees, positions, users |
| Transactions | Beginning Balances, Delivery Receipts, RIS (with job orders), Stock Requests → Releases, adjustments |
| Ledger | Stock movements and lines, per-office on-hand balances, snapshot-at-posting weighted average, ledger timeline, valuation dashboards |
| Field capture | QR item labels, device pairing, UP Mail mobile sign-in, offline sync, scan ingest through Kafka, scans folded into drafts |
Plus the usual scaffolding: two Filament panels (admin at /admin, custodians at
/), roles and permissions, office-scoped queries on every tenant model, an
append-only audit trail, RediSearch full-text search, Excel item import, and
impersonation for support.
Results
- Every figure traces back to one posted ledger. Beginning Balances, Delivery Receipts, RIS, and adjustments all post into the same stock ledger, and nothing in the app reports a number just because a document claimed it.
- Reported costs stop moving. Snapshot-at-posting freezes the weighted average onto each ledger line, so a figure printed on a Stock Card in March still reads the same in August — the property a back-dated receipt used to quietly break.
- The prescribed COA forms come out of the system: Stock Card (GAM Appendix 58), RSMI (Appendix 64), WMR (Appendix 65), and RIS with its job order, in the prescribed layouts.
- The warehouse floor works without signal. Handsets hold a local catalogue copy, queue scans in an outbox, and flush them idempotently — one unique index makes offline retry, network duplication, and Kafka redelivery all harmless.
- No permanent credential ends up on a screen. Pairing codes are single-use, stored only as a hash, and short-lived; the token comes back over HTTPS and never rides in the QR.
- Scans never move stock on their own. They land as reviewable drafts, so a bad scan is a correction in a panel rather than a wrong balance.
A Glimpse of the App


The custodian hub. Every chip is ledger-derived — Total Amount is the weighted-average valuation, not a sum of document totals.
Snapshot-at-posting shows up in that timeline if you know where to look. The
posted Beginning Balance has real per-line costs (₱200.00 × 2 = ₱400.00); the RIS
above it is still a draft, so its cost columns read —. No posting, no frozen
cost.


The admin panel — same ledger, unscoped across every office.
Two tiles here are greyed out and badged Soon, and they say what they’re
waiting on: below reorder level needs reorder levels, semi-expendable on issue
needs item classification. Neither shows a 0. A dashboard that prints zero for
something it doesn’t model is worse than one that prints nothing, because someone
will believe the zero.


Issuing a pairing code from the Users list. Other rows are redacted; the code shown expired the same minute the screenshot was taken.
The code is offered as a QR and as text you can type, the countdown shows the window it was issued with, and the amber notice spells out what the issuer is taking on — everything scanned on that handset gets recorded against that person. This one has a 15-minute window, so it was administrator-issued; self-service pairing defaults to five.
How Costing Works
COA prescribes the weighted moving average: recompute the average on every receipt, value issues at the running average. SIMS originally derived that at read time — correct, but fragile. A back-dated receipt silently re-costed issues that had already been reported.
It now snapshots at posting. When an issue posts, the average at that instant gets frozen onto its ledger line and is never recomputed; corrections go in as new adjustments. A figure printed on a Stock Card in March still reads the same in August. A migration backfilled existing history without touching a single receipt cost.
Records It Generates
- Stock Card (GAM Appendix 58) — per item, per office, replayed from the ledger. Single or batch, one card per page.
- RSMI (Appendix 64) — per office, per month, RIS issues with a per-item recapitulation.
- WMR (Appendix 65) — the prescribed form, blank. There’s no waste-tracking data model yet, so it prints for hand completion rather than inventing figures.
- RIS and its job order — straight from the live document, for signature.
- QR label sheets — configurable grid, so the output matches whatever sticker stock the office actually buys.
- Pairing sheets — two cards per row, cuttable so one person’s code can be handed over without exposing anyone else’s.
dompdf handles the form layouts, where HTML and CSS express the prescribed grids. FPDF handles the label sheets, where millimetre placement and a memory ceiling matter more.
Pairing a Phone
The QR never carries the token. You generate a code, the phone posts it to
POST /api/devices/pair, and a Sanctum token comes back over HTTPS. Putting the
token in the QR instead would leave a permanent credential on a screen for anyone
in the room — or in a photo — to keep.
What makes the device record trustworthy:
- Single use, and the row is locked on redeem so two phones can’t both win the race.
- Five minutes by default. Admins issuing on someone’s behalf can pick a longer window, up to 24 hours for a scheduled enrolment session.
- Only the hash is stored. The plain code lives as long as it takes to draw a QR.
- Typeable as well as scannable — scanning needs a working camera and a screen it can focus on, typing needs neither.
- One row per install, so re-pairing after a reinstall revokes the old token instead of piling up dead devices.
- Abilities are allow-listed (
sync:read,scan:write) and enforced per route, so a read-only sync token can’t post scans. - The user and device come from the token, never the payload. A handset can’t file a scan under someone else’s name.
There’s a second route for the Expo app: sign in with UP Mail natively, post the
Google ID token to POST /api/auth/google, get a device token. It matches an
existing SIMS user and creates nothing. After that Google is out of the picture —
a Google session lasts an hour, and an outbox might need to flush days later with
nobody watching.
Offline Scanning
The phone keeps its own SQLite copy of the catalogue, pulled from
GET /api/sync/{resource} with keyset paging (offset paging skips rows that shift
mid-pull) and an explicit column allow-list per resource. Scans queue in an outbox
and flush to POST /api/scan-batches, which answers 202 with per-event verdicts
so the phone knows exactly which rows to clear — a duplicate counts as a win,
since it means an earlier attempt landed.
From there:
- The
scan_eventsrow is written before the Kafka message, so nothing is acknowledged without being durable somewhere. Kafka buys replay and fan-out; it isn’t the system of record. - A scheduled command folds published events into draft documents.
- Nothing posts to stock automatically. A scan is an unverified observation from a warehouse aisle, so posting stays a human decision in the panel — which is what makes the async path safe. A bad scan becomes a reviewable draft, not a wrong balance.
One unique index on scan_events.event_id is what makes offline retry, network
duplication, and Kafka redelivery all harmless.
The printed label carries a format version, record kind, SKU, and a short HMAC — but no cost or quantity, since those change and the label would be wrong the moment a price is updated. 25% error correction, because warehouse stickers get scuffed and peeled, and the SKU in plain text so a dead code can still be keyed in.
What Was Tricky
- Making costs auditable, not just correct. Moving to snapshot-at-posting meant finding every posting path, freezing the average inside the transaction that already held the stock lock, teaching every read model to prefer the frozen cost, and backfilling history without touching receipts.
- Trusting a device without trusting its payload. Pairing and sign-in have to be unauthenticated, so each gets the protection that fits: an unguessable single-use code with a generous throttle for pairing (mistyping is an operator error, not an attack), and a much tighter one on Google sign-in, where failures are someone probing and each attempt costs a JWKS fetch.
- A double-count hiding in the timeline. Posting an RIS creates an internal issue-ledger entry as an adjustment. The timeline rendered it as a spurious card and counted it as positive, re-adding issued stock to the running balance.
- Two panels, one domain. Office scoping lives on the models via a shared concern rather than in the panels, so a new resource is scoped by default instead of by remembering.