Inventory
The Inventory Aether tracks hardware assets — the categories that shape them, the purchase orders they arrive on, the licences and warranties attached to them, and who currently holds them.
public/screenshots/inventory-assets.pngHow it fits together
Section titled “How it fits together”- Asset Categories classify assets and define custom fields that every asset in the category inherits.
- Purchase Orders record procurement; receiving a PO rolls its line items into stock as assets.
- Assets are the records themselves — assignable to People (from the Tenancy Aether), with a full assignment history.
- Software Licences and Warranty Tracker hang off the asset estate.
Modules
Section titled “Modules”Asset Categories
Section titled “Asset Categories”Classification for assets, plus the custom field definitions every asset in
the category inherits — define a field once on the category and it surfaces on
every asset beneath it. Each category lists its fields inline; there is no
separate field-definition endpoint.
Fields
| Field | Type | Notes |
|---|---|---|
name | string · required | Category name. |
slug | string · unique | Auto-derived from name when omitted; drives the asset-tag prefix. |
icon | string | Lucide icon key (default package). |
description | string | Free-form. |
Each field definition (assetFieldDefinitions, returned inline as
fields) carries:
| Field | Type | Notes |
|---|---|---|
categoryId | uuid → Asset Category · required | Cascade-deletes with the category. |
fieldKey | string · required | Stable key used to store values per asset. |
label | string · required | Display label. |
fieldType | string | Input type (default text). |
options | string | JSON string of choices; parsed to an array in responses. |
required | boolean | Default false. |
placeholder | string | Input placeholder. |
helpText | string | Helper copy. |
sortOrder | integer | Display order (default 0). |
API — GET/POST /api/asset-categories, GET/PATCH/DELETE /api/asset-categories/{id}. List and detail responses embed the category’s fields.
Notes — slug is unique and feeds a per-category asset-tag prefix (for
example laptop → LAP). Field definitions have no own CRUD endpoints — they
are read back inline on the category; options is stored as a JSON string and
parsed to an array in responses. Writes require admin.
public/screenshots/inventory-categories.pngPurchase Orders
Section titled “Purchase Orders”Procurement records with line items. Totals recalculate from the line items whenever items change, and receiving a PO rolls its items into stock as asset records. In the web app this is a dedicated list/detail page (not a tab on Assets) with create, edit, receive, and Import PO.
Fields
| Field | Type | Notes |
|---|---|---|
poNumber | string · required · unique | Purchase-order number. |
supplierName | string | Free-form supplier. |
status | enum | draft · ordered · partially_received · received · cancelled (default draft). |
orderDate | timestamp | When ordered. |
expectedDelivery | timestamp | Expected arrival. |
receivedDate | timestamp | Set when fully received. |
totalValue | numeric | Recalculated from line items — not set directly. |
notes | string | Free-form. |
createdBy | uuid | Profile that raised the PO. |
Each line item (purchaseOrderItems):
| Field | Type | Notes |
|---|---|---|
poId | uuid → Purchase Order · required | Cascade-deletes with the PO. |
description | string · required | Becomes the asset name on receipt. |
categoryId | uuid → Asset Category | Carried onto created assets. |
manufacturerId | uuid → Manufacturer | Carried onto created assets. |
quantityOrdered | integer | Default 1. |
quantityReceived | integer | Incremented on receive (default 0). |
unitCost | numeric | Per-unit cost. |
totalCost | numeric | unitCost × quantityOrdered. |
createsAsset | boolean | Default true. Set false for charges (delivery, freight, tax) — counted on the PO total but no asset is created on receive. |
notes | string | Free-form. |
API — GET/POST /api/purchase-orders, GET/PATCH/DELETE /api/purchase-orders/{id}, plus line items via POST /api/purchase-orders/{id}/items, PATCH/DELETE /api/purchase-orders/{id}/items/{itemId}, and POST /api/purchase-orders/{id}/receive.
Import (web UI) — use Import PO on the Purchase Orders page:
| Format | How it works |
|---|---|
| CSV | One row per line item; rows sharing po_number group into one PO. Sample downloadable from the modal. |
| JSON | Array of PO objects with nested items. Sample downloadable from the modal. |
| Client-side text extraction + heuristics. Always opens a review step so you can fix fields and map category/manufacturer before create. Lands as draft only. Scanned/image PDFs (no text layer) need manual entry for now. |
CSV columns (aliases accepted): po_number, supplier_name, status,
order_date, expected_delivery, notes, description, category,
manufacturer, quantity, unit_cost. Category and manufacturer names are
matched to existing catalog records when possible. Unmatched categories can be
created inline (+ Create category) during PDF review. Lines matching fee
keywords (delivery, freight, GST, …) are flagged as charges (createsAsset: false) so they do not spawn assets when received.
Notes — totalValue and each item’s totalCost recalculate from
unitCost × quantity; you don’t set the header total directly. Creating a PO
requires at least one line item. Receiving validates each unit against the
line’s remaining quantity, creates one asset per unit received (inheriting the
item’s description, categoryId, and manufacturerId), bumps
quantityReceived, and moves the PO to partially_received or received — a
PO can only be received from ordered or partially_received. A PO that has
received items cannot be deleted. Access is role-gated (procurement / sysadmin /
it_manager / administrator / super_admin can write; auditor / read_only are
read-only).
public/screenshots/inventory-purchase-order.pngAssets
Section titled “Assets”The asset records — make, model, serial, status — assignable to people with a full assignment history, and carrying any custom field values inherited from their category.
Fields
| Field | Type | Notes |
|---|---|---|
name | string · required | Display name. |
assetTag | string · unique | Auto-generated from the category prefix when omitted. |
serialNumber | string | Manufacturer serial. |
status | enum | active · spare · maintenance · retired · decommissioned · lost · stolen (default active). |
condition | string | Default good. |
categoryId | uuid → Asset Category | Drives inherited custom fields and the tag prefix. |
manufacturerId | uuid → Manufacturer | Maker. |
siteId | uuid → Site | Current site. |
locationId | uuid → Location | Current location. |
purchaseOrderId | uuid → Purchase Order | Set when received from a PO. |
purchasedAt | timestamp | Purchase date. |
warrantyUntil | timestamp | Inline warranty expiry. |
purchasePrice | numeric | Acquisition cost. |
notes | string | Free-form. |
Custom field values are stored per asset in assetFieldValues (assetId →
Asset, fieldKey, value) and submitted as a fieldValues map on
create/update.
API — GET/POST /api/assets, GET/PATCH/DELETE /api/assets/{id}, plus POST /api/assets/{id}/assign, POST /api/assets/{id}/return, and GET /api/assets/{id}/assignments.
Notes — assetTag is unique and auto-generated when omitted, using a
per-category prefix (for example LAP-000001) with collision retry. Custom
fields are per-category: definitions live on the category, values are stored
per asset and submitted as a fieldValues map. Assigning closes any open
assignment before opening a new one; assign/return target a person
(Tenancy Aether) or profile, and GET /api/assets/{id}/assignments returns the
full history. The list endpoint also accepts category, status, and
assigned filters. Deleting an asset requires admin.
Software Licences
Section titled “Software Licences”Software licence records — seats, keys, expiry, and the vendor that supplies them. Seat usage is tracked but not enforced.
Fields
| Field | Type | Notes |
|---|---|---|
name | string · required | Licence name. |
product | string | Product covered. |
licenceKey | string | Key or serial. |
seats | integer | Total seats. |
assignedSeats | integer | Seats in use (default 0). |
vendorId | uuid → Vendor | Supplier; set null on vendor delete. |
expiryDate | date | Renewal date. |
cost | numeric | Licence cost. |
status | string | Default active. |
notes | string | Free-form. |
API — GET/POST /api/software-licences, GET/PATCH/DELETE /api/software-licences/{id}.
Notes — seats and assignedSeats are recorded for visibility;
assignedSeats is a plain count, not derived from asset assignments, and is
not enforced against seats.
public/screenshots/inventory-licences.pngWarranty Tracker
Section titled “Warranty Tracker”Warranty records against assets — coverage dates and provider, so expiries are visible before they bite.
Fields
| Field | Type | Notes |
|---|---|---|
name | string · required | Warranty name. |
assetId | uuid → Asset | Covered asset; cascade-deletes with the asset. |
provider | string | Warranty provider. |
type | string | Coverage type. |
startDate | date | Coverage start. |
endDate | date | Coverage end. |
cost | numeric | Warranty cost. |
status | string | Default active. |
notes | string | Free-form. |
API — GET/POST /api/warranties, GET/PATCH/DELETE /api/warranties/{id}.
Notes — assetId is optional but cascade-deletes with its asset, so
warranties vanish when the covered asset is removed. startDate/endDate are
plain dates; an asset also carries its own inline warrantyUntil, separate
from this tracker.
Resources are exposed under /api/assets, /api/asset-categories,
/api/purchase-orders, /api/software-licences, and /api/warranties — see the
API reference.