Place hierarchy & stock counts
Places are Loca.fi's locations and zones — warehouses, stores, aisles, dock doors — arranged in one tree per organisation. Everything else hangs off that tree: items live at a place, stocktakes count a place, and user visibility grants expand down the tree.
Read Place hierarchy & visibility
first for how the tree is stored (ParentPlaceId as source of truth, derived
LeftBower/RightBower nested-set ranges) — this guide is the doing, that
page is the understanding.
Overview
The working loop:
- Build the tree —
POST api/places/CreatePlace, roots first (omitParentPlaceId), then children. - Read the tree —
GET api/places/getfilteredplaces; a subtree is a bower-range filter. - Move places —
POST api/places/updateplacewith a newParentPlaceId; the server rebuilds the whole tenant's bowers. - Tag zones for RFID —
POST api/places/updateplacetag. - Read stock per place —
GET api/places/getdashboard/{id}and the count endpoints below.
Place CRUD is gated on the Place module permission;
UpdatePlaceStructure requires
Management Update; the retail/v1/stock/* reads require Inventory
Read.
Prerequisites
- A token with
Placeread/write permission — see Getting started. - A Place template id for
TemplateIdon create — templates define the place's custom fields; see Templates & extended properties. - The one rule to internalise before touching the tree: never cache
LeftBower/RightBoweracross writes — any place create or parent change anywhere in the tenant renumbers every place's bowers. Re-read after every mutation.
Step-by-step
1. Create root-level places
Omit ParentPlaceId and the place is auto-parented to the tenant root:
curl -sk -X POST "http://<host>/api/places/CreatePlace" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Name":"Store 1","TemplateId":"<placeTemplateId>",
"PlaceCode":"S1","ExternalRef":"store_01"}'
Returns 200 with a PlaceDetailDto — capture Id. The response already
carries the freshly rebuilt LeftBower/RightBower. Name must be unique
per organisation. Creating a place also auto-grants visibility of it to the
creator (relevant for place-filtered users — see
the concepts page).
2. Create children
Same endpoint, plus the parent's id:
curl -sk -X POST "http://<host>/api/places/CreatePlace" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Name":"Aisle 1","TemplateId":"<placeTemplateId>",
"ParentPlaceId":"<store1Id>"}'
Every create triggers a tenant-wide nested-set rebuild — the parent's bowers widen to enclose the new child, and unrelated places may be renumbered too.
3. Read the tree and query subtrees
getfilteredplaces returns an OData-paged
PageResult<PlaceSummaryDto> (visibility-filtered, system places excluded).
Each row carries ParentPlaceId, ParentPlaceName, LeftBower, and
RightBower — enough to render the tree client-side.
To fetch a place and everything under it, read the ancestor's bowers
first (getplace/{id}), then filter on
the range ($ is %24 in curl):
# Parent's bowers, e.g. LeftBower=2, RightBower=9 → subtree is (2..9) inclusive
curl -sk "http://<host>/api/places/getfilteredplaces?%24filter=LeftBower%20ge%202%20and%20RightBower%20le%209" \
-H "Authorization: Token $TOKEN"
Both reads must happen in the same "generation" of the tree — if any place
mutation lands in between, the range may be stale. Treat bowers as a
disposable query mechanism and ParentPlaceId as the durable shape.
4. Move a place
A move is just an updateplace with a
different ParentPlaceId. The update is full-replace: send every field
you want to keep, because omitted fields are nulled on the entity. Fetch the
current detail first, mutate, post back:
curl -sk -X POST "http://<host>/api/places/updateplace" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Id":"<aisle1Id>","Name":"Aisle 1","TemplateId":"<placeTemplateId>",
"ParentPlaceId":"<store2Id>"}'
Changing ParentPlaceId triggers the tenant-wide bower rebuild. Setting it
to the place itself or to one of its own descendants is rejected with 400
InvalidOperation ("ParentPlaceId would create a cycle") — the server
walks the proposed parent chain upward.
After any create, move, or structure write, previously fetched bowers are unreliable for the entire tenant — not just the branch you touched. Re-fetch before the next subtree query. Note also that a place-filtered user may briefly see a stale subtree after a move: the visibility cache is re-seeded asynchronously.
5. Bulk re-arrange with UpdatePlaceStructure
For a drag-and-drop tree editor that reorganises many places at once,
POST api/places/UpdatePlaceStructure
writes client-computed bowers in one call. It requires the Management
module's Update permission, and it is the one endpoint where the server
trusts your bower values verbatim — no rebuild, no recomputation:
curl -sk -X POST "http://<host>/api/places/UpdatePlaceStructure" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '[{"Id":"<store1Id>","LeftBower":2,"RightBower":7},
{"Id":"<aisle1Id>","LeftBower":3,"RightBower":4},
{"Id":"<aisle2Id>","LeftBower":5,"RightBower":6}]'
Returns 200 with true (an empty payload is a 200 no-op; an unknown
place id in the payload is 400 NotFound). Only use this if you are
supplying a complete, valid nested set for the affected branch — a
malformed set corrupts every bower-range consumer (subtree queries,
visibility expansion, dashboards) until the next server-side rebuild.
6. Delete places
curl -sk -X DELETE "http://<host>/api/places/deleteplace/<aisle1Id>" \
-H "Authorization: Token $TOKEN"
Returns 200 with true. The deleted place's direct children are
re-parented to its parent (their own subtrees ride along), so the tree
never orphans a branch. Deletion does not rebuild the nested set — the
surviving bowers keep a gap until the next create or parent change closes it.
Guards: a place holding items is rejected (IllegalDeleteAttempt,
"Items in Place"), and the tenant root cannot be deleted — delete
validators accumulate, so a root-delete attempt returns both errors in
one array. Batch deletion:
batchdeleteplaces.
7. Tag a place for RFID zone tracking
Fixed readers report zones by tag, so give the place an RFID tag with
updateplacetag. A place holds one
tag, surfaced as a one-element list; posting a new entry replaces the
current tag (recording a tag-replacement history row), and an empty list
clears it:
curl -sk -X POST "http://<host>/api/places/updateplacetag" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Id":"<dockDoorId>",
"PlaceTagList":[{"TagType":"PassiveRfid","TagNumber":"303402662C00000000000001"}]}'
To find which place a scanned tag belongs to, use
GetPlacesWithProperty:
curl -sk "http://<host>/api/places/GetPlacesWithProperty?propertyName=TagNumber&propertyValue=303402662C00000000000001" \
-H "Authorization: Token $TOKEN"
propertyName accepts standard place properties or an extended-property
name; the response is a list of PlaceDetailDto.
8. Read stock counts per place
Three complementary reads, plus the count-maintenance writes:
getdashboard/{id}— the aggregate stock/order position of one place (PlaceDashboardDto): items currently in the place (TotalItemsInPlace), missing items, last stocktake date, the top per-SKU counts at this place only (SkuStockCountList) and rolled up across the subtree (RolledUpStockCountList), recent stock-count history, and pending inbound/outbound orders.GET api/skus/getfilteredskustockcountforplace/{placeId}/includechildplaces/{true|false}— per-SKU, per-item-state counts for a place, optionally including its subtree (SKU Read permission).GET retail/v1/stock/getcurrentstock— live per-SKU counts from the materialised stock table. PassplaceId(orplaceName), pluscountSubLocations=trueto include descendants in the totals orincludeSubLocations=truefor a per-place breakdown, andbarcode/skuNo/fieldsfilters. RequiresInventoryRead. Its companion,getlateststocktake, reports the position as at the last completed stocktake — the full count workflow that feeds both lives in Running a stocktake.
curl -sk "http://<host>/retail/v1/stock/getcurrentstock?placeId=<store1Id>&countSubLocations=true" \
-H "Authorization: Token $TOKEN"
The retail/v1/stock/* endpoints are the retail surface — quantity-per-SKU
counts maintained by stocktakes and reader traffic. Asset-management tenants,
which track each item individually, get more value from the place dashboard's
item-state breakdowns and the per-state SKU stock-count endpoint above.
For externally fed counts (an ERP pushing stock levels), maintain the
place-SKU rows with
setplaceskucurrentcount
(absolute value, auto-creates the row) or
adjustplaceskucurrentcount
(signed delta — Adjustment: 2 on a count of 5 yields 7; it never
auto-creates the row, and a delta that would go below zero is rejected).
Place and SKU are resolvable by id, code, number, or barcode — ERP-friendly.
Related per-place reads worth knowing: weekly accuracy trend via
get-inventory-score/{id}/{weeks},
and writing off long-missing stock with
consume-missing-items.
Error handling
All failures follow the platform-wide
error and response conventions —
400 with a JSON array of { ErrorCode, Message, Field }.
| Situation | Response |
|---|---|
Duplicate place Name (or PlaceCode/ExternalRef) | 400 InvalidOperation — the offending field name rides in Message, not Field |
Invalid PlaceType / Channel enum string | 400 InvalidOperation naming the field |
Unknown ParentPlaceId on create/update | 400 with an empty error array [] — the FK failure is swallowed with no error codes |
ParentPlaceId set to the place itself or a descendant | 400 InvalidOperation, "ParentPlaceId would create a cycle" |
| Get / update / delete with an unknown place id | 400 NotFound ("place") |
| Delete the tenant root | 400 with two accumulated errors: IllegalDeleteAttempt ("Items in Place") and InvalidOperation ("Tenant root Place") |
UpdatePlaceStructure with an unknown place id | 400 NotFound ("PlaceId") |
| Adjust that would take a count below zero | 400 InvalidOperation; the count is left unchanged |
| Adjust against a place-SKU row that doesn't exist | 400 — the Adjust endpoints never auto-create the row (use Set) |
getcurrentstock without placeId or placeName | 400 InvalidValue |
getcurrentstock with an unknown place | 200 with a null body — known quirk; the internal InvalidReference code is never surfaced |
No token / wrong scheme (Bearer) | 401 |
Related
- Place hierarchy & visibility — the tree model, bower rebuild semantics, and user place visibility behind this guide.
- Running a stocktake — the counting workflow that produces the numbers read in step 8.
- Stocktakes — inventory vs cycle count concepts.
- Templates & extended properties — the place template consumed at create.
- Reference pages: Create Place, List Places, Get Single Place, Update Place, Update Place Structure, Delete Place, Update Place Tag, Get Places With Property, Get Dashboard, Get Place Sku Stock Count, Get Current Stock.