### Zoho Inventory — organizations, stock, warehouses and order flows (read before writing)

Inventory is the **Books API family**: same `organization_id` scoping, same envelope, same record-shaped write bodies. What is different — and where the mistakes are — is that every document either **moves stock or does not**, and stock always belongs to a warehouse.

#### Every call is scoped to one organization

```bash
zone inventory orgs list --toon                  # organization_id, name, currency
zone ctx inventory organization_id=<id>          # injected into every inventory call from here on
```

Inventory organizations are the **same organizations as Books** when both are enabled — an org id from `zone books org list` works here. Without the ctx, Zoho answers "belongs to multiple organizations"; set it rather than retrying.

#### Envelope and pagination (same as Books)

Responses are `{"code": 0, "message": "success", …}` with the payload under the **plural** resource key for lists (`items`, `salesorders`, `purchaseorders`, `warehouses`) and the **singular** for one record. Lists carry `page_context` with `has_more_page`; page with `--page` / `--per-page` (max 200) and filter with the list flags (`--status`, `--customer-id`, `--vendor-id`, `--salesorder-id`, `--search`) instead of fetching everything.

A write body **is the record** — no `{"data":[…]}` wrapper, no batching; one call, one record. `--data` takes inline JSON, `@file.json` or `-`.

#### Stock lives in warehouses

```bash
zone inventory warehouse list --toon             # warehouse_id, name, is_primary
zone inventory location list --toon              # multi-location orgs (newer model)
zone inventory item get <itemId>                 # stock per warehouse in warehouses[]/locations[]
zone inventory item details --ids <id1>,<id2>    # several items at once
```

An item's stock is **not one number**. Each warehouse (or location) carries its own `stock_on_hand`, `available_stock` and `actual_available_stock`. When an org has more than one warehouse, every stock-moving document needs the warehouse on it (`warehouse_id` on the document or its line items) — omit it and Zoho either rejects the call or silently uses the primary warehouse, which is how stock ends up in the wrong place.

Only items with `track_inventory: true` and `item_type: "inventory"` have stock at all. A `service` item has none, and adjusting it fails.

#### Creating a stock item

```bash
zone inventory item create --data '{
  "name":"Widget A","sku":"WID-A","item_type":"inventory","product_type":"goods",
  "unit":"pcs","rate":300,"purchase_rate":180,"track_inventory":true,
  "account_id":"<sales account>","purchase_account_id":"<cogs account>","inventory_account_id":"<stock account>",
  "initial_stock":100,"initial_stock_rate":180,
  "warehouses":[{"warehouse_id":"<id>","initial_stock":100,"initial_stock_rate":180}]
}'
```

`initial_stock` is only honoured **at creation**. After that, stock changes only through documents: a purchase receive, an adjustment, a transfer, or a sale. Updating `initial_stock` later does nothing.

#### Sales flow — and which step actually moves stock

```bash
zone inventory so create --data '{"customer_id":"…","date":"2026-09-01","line_items":[{"item_id":"…","quantity":10,"rate":300,"warehouse_id":"<id>"}]}'
zone inventory so confirm <soId>                          # draft -> confirmed (commits, does not move stock)
zone inventory package create --salesorder-id <soId> --data '{"date":"2026-09-02","line_items":[{"so_line_item_id":"…","quantity":10}]}'
zone inventory shipment create --salesorder-id <soId> --package-ids <pkgId> --data '{"date":"2026-09-02","delivery_method":"DHL","tracking_number":"…"}'
zone inventory shipment delivered <shipmentId>            # <- stock leaves here
zone inventory invoice create --data '{"customer_id":"…","salesorder_id":"<soId>","line_items":[…]}'
```

The order is **sales order → package → shipment → invoice**. Confirming a sales order reserves nothing physical; the **shipment** is what decrements stock, and packages are what a shipment is built from. An invoice is money, not movement — invoicing without shipping leaves the stock where it was.

Returns run the other way: `return create --salesorder-id <soId>`, then `return receive --salesreturn-id <id>` puts the goods back.

Delivery challans (`challan`) move goods without invoicing — `challan delivered <id>` for the outbound, `challan returned <id>` to reverse.

#### Purchase flow

```bash
zone inventory po create --data '{"vendor_id":"…","date":"2026-09-01","line_items":[{"item_id":"…","quantity":50,"rate":180,"warehouse_id":"<id>"}]}'
zone inventory po issue <poId>
zone inventory receive create --purchaseorder-id <poId> --data '{"date":"2026-09-05","line_items":[{"po_line_item_id":"…","quantity":50}]}'
zone inventory receive received <receiveId>               # <- stock arrives here
zone inventory bill create --data '{"vendor_id":"…","purchaseorder_id":"<poId>","line_items":[…]}'
zone inventory landedcost create <billId> --data '{"description":"Freight","amount":450,"allocation_type":"quantity"}'
```

**Purchase receive** is what increases stock, not the purchase order and not the bill. Landed costs attach to the bill and spread freight or duty across the received items, changing valuation.

`putaway` moves received goods into bin locations after a receive, when storage locations are enabled.

#### Moving and correcting stock

```bash
zone inventory transfer create --data '{"date":"2026-09-02","from_warehouse_id":"<id>","to_warehouse_id":"<id>","line_items":[{"item_id":"…","quantity":5}]}'
zone inventory transfer intransit <id>   ;  zone inventory transfer transferred <id>

zone inventory adjustment create --data '{"date":"2026-09-02","reason":"Damaged in store","adjustment_type":"quantity","line_items":[{"item_id":"…","warehouse_id":"<id>","quantity_adjusted":-2}]}'
zone inventory adjustment approve <id>

zone inventory moveorder create --data '{…}'      ;  zone inventory moveorder complete <id>   # within a warehouse, between bins
```

An adjustment is the **only** legitimate way to change stock without a document — `adjustment_type` is `quantity` or `value`, and a negative `quantity_adjusted` reduces. Transfers move between warehouses (`intransit` → `transferred`); move orders shuffle bins inside one warehouse. Adjustments and transfers may need `submit` → `approve` when approval is switched on.

#### Counting stock

```bash
zone inventory stockcount create --data '{"warehouse_id":"<id>","date":"2026-09-30"}'
zone inventory stockcount start <id>
zone inventory stockcount count <id> --data '{"line_items":[{"item_id":"…","counted_quantity":98}]}'
zone inventory stockcount submit <id>   ;  zone inventory stockcount approve <id> --data '{}'
```

Approving a count writes the difference as an adjustment. `recurringcount` schedules cycle counts; `picklist` drives warehouse picking for confirmed sales orders.

#### Batch and serial tracking

```bash
zone inventory batch list --item-id <itemId>
zone inventory serial list --item-id <itemId> --warehouse-id <id>
zone inventory item serial-validate <itemId> --data '{"serial_numbers":["SN-1","SN-2"]}'
```

If an item is batch- or serial-tracked, **every** stock-moving line item must carry the tracking detail, or the document will not post. Packages, picklists and stock counts have their own `advancedtrackingdetails` / `trackingdetails` endpoints for exactly this.

#### Item structures

| Concept | Command | What it is |
|---|---|---|
| item group | `group list/create` | variants of one product (size, colour) |
| composite item | `composite list/create` | a made-up item assembled from components |
| bundle | `bundle create` → `bundle bundled` → `bundle confirmed` | one act of assembling a composite, consuming components and producing stock |
| price book | `pricebook list/create` | per-customer or per-currency pricing |
| unit / unit group | `unit list`, `unitgroup list`, `unit create-conversion` | units of measure and conversions between them |

#### Snapshot every organization's setup

```bash
zone inventory pull -o ./inventory-settings      # every org: warehouses, locations, bins, taxes, units, price books, users, preferences
zone inventory pull --org <id>                   # one org
zone inventory pull --only warehouses            # one collection across all orgs
zone inventory pull --with-records               # also items, contacts, sales orders, purchase orders
```

Writes `<out>/<Org Name (id)>/…` per organization, like `zone books pull`; orgs where your access is disabled are skipped after one denial. `diff -r` between two org folders shows exactly how their setups differ.

#### Purchase returns, item variants and categories (shared with Books)

zone 0.8.3 added `purchasereturn`, `itemmaster`, `itemvariant` and `category` — the same paths Books serves, from one shared spec (`src/specs/finance-shared/`). Everything under **Books → Purchase returns, item variants and categories** applies unchanged: per-record `POST …/status/{open|void|fulfilled|unfulfilled}` vs collection-level `PUT …/status/{closed|reopen}`, variants owned by an item master, `/itemvariants/move/{id}`.

These rows come from Zoho's Inventory OpenAPI bundle; they were exercised live on Books (same platform) but not yet on an Inventory organization.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| "belongs to multiple organizations" | no `organization_id` | `zone ctx inventory organization_id=<id>` |
| "warehouse is required" / stock lands in the wrong warehouse | multi-warehouse org, no `warehouse_id` on the document or line | add it; `zone inventory warehouse list` |
| "insufficient stock" / "available quantity" | the shipment, transfer or adjustment exceeds `available_stock` in that warehouse | check `item get <id>` per warehouse; receive or adjust first |
| "item is not an inventory item" | the item is a service, or `track_inventory` is false | only inventory items hold stock |
| tracking-detail errors on a post | batch- or serial-tracked item without tracking on the line | supply batch/serial per line |
| `HTTP 404 Resource does not exist` | wrong id, or the id belongs to another org | list it in the **same** org |
| "already exists" | duplicate `sku` or document number | update the existing record |
| "You do not have access as your account is disabled" | the org exists but your user is disabled in it | pick another org |
| exit 3 | not signed in — Inventory is its own OAuth grant | human runs `zone login inventory` |
| exit 4 | token lacks `ZohoInventory.fullaccess.ALL` | human re-logs in that service |

A command that exits 0 succeeded (`code: 0`); still read the returned id, and for stock work re-read the item to confirm the movement landed in the warehouse you meant.

> Envelope, pagination and body shape above are the Books family conventions, verified live on a Books org; Inventory's own responses were not re-verified because this account has no Inventory session. Command names, paths and arguments come from the installed specs, so they are exact.
