Adjust On-Hand Quantity (Write-Off)

Post an inventory adjustment — a signed on-hand quantity change with no invoice — via the InventoryAdjustment service.

API: Transaction · Service: InventoryAdjustment · Deep dive: Inventory Adjustment (Write-Offs) · Full schema: definitions/InventoryAdjustment.json

Prerequisites

Payload

POST {ui_server}/api/v2/transaction, Status: "New", UseCodeValues: false. Two DataElements:

Header — TABPAGE_1.tp_1_dw_1 (Form, business object inv_adj_hdr, key adjustment_number)

Field Type Required Notes
adjustment_number Decimal Key Server-generated — leave unset on a new adjustment
company_id Char Yes Company ID
location_id Decimal Yes Location being adjusted
reason_id Char Yes The reason's display text (with UseCodeValues: false), not its code
period / year_for_period Decimal Yes (per definition) Accounting period / year for the adjustment
inv_adj_description Char No Free-text description
approved Char No ON / OFF

The verified minimal header from the manual is location_id + reason_id; the definition additionally marks company_id, period, and year_for_period as Required.

Lines — TABPAGE_17.tp_17_dw_17 (List, business object inv_adj_line)

Field Type Required Notes
item_id Char Yes Item to adjust
unit_quantity Decimal Yes The signed delta (label "Adjustment Amount") — e.g. -5 writes off 5 units; negative on-hand zeroes it out
unit_of_measure Char No
unit_size Decimal Yes (per definition) From item_uom.unit_size
new_qoh Decimal No Resulting quantity on hand (display)

The verified minimal line from the manual is item_id + unit_quantity.

Complete example

Writes off 5 units of WIDGET-001 at location 10, then reads the adjustment back by its server-generated adjustment_number. Auth comes from the shared helper.

import httpx

BASE_URL = "https://play.p21server.com"
token, ui_server, headers = p21_auth(BASE_URL, "api_user", "api_pass")

payload = {
    "Name": "InventoryAdjustment",
    "UseCodeValues": False,  # reason_id is the display text, not the code
    "Transactions": [{
        "Status": "New",
        "DataElements": [
            {
                "Name": "TABPAGE_1.tp_1_dw_1",
                "Type": "Form",
                "Keys": [],
                "Rows": [{
                    "Edits": [
                        {"Name": "company_id", "Value": "ACME"},
                        {"Name": "location_id", "Value": "10"},
                        {"Name": "reason_id", "Value": "ADJUST"},  # display text
                        {"Name": "inv_adj_description", "Value": "Cycle count write-off"},
                    ],
                    "RelativeDateEdits": [],
                }],
            },
            {
                "Name": "TABPAGE_17.tp_17_dw_17",
                "Type": "List",
                "Keys": [],
                "Rows": [{
                    "Edits": [
                        {"Name": "item_id", "Value": "WIDGET-001"},
                        {"Name": "unit_quantity", "Value": "-5"},  # signed delta, NOT new on-hand
                    ],
                    "RelativeDateEdits": [],
                }],
            },
        ],
    }],
}

resp = httpx.post(f"{ui_server}/api/v2/transaction",
                  headers=headers, json=payload, verify=False, timeout=120)
resp.raise_for_status()  # HTTP 200 even on failure -- check the Summary
result = resp.json()

summary = result["Summary"]
print(f"Succeeded: {summary['Succeeded']}, Failed: {summary['Failed']}")
if summary["Failed"] > 0:
    for msg in result.get("Messages", []):
        print(f"  {msg}")
    raise SystemExit("Adjustment failed")

# Pull the server-generated adjustment_number out of the echoed DataElements
adjustment_number = None
for txn in result.get("Results", {}).get("Transactions", []):
    for de in txn.get("DataElements", []):
        if de.get("Name") == "TABPAGE_1.tp_1_dw_1":
            for row in de.get("Rows", []):
                for edit in row.get("Edits", []):
                    if edit["Name"] == "adjustment_number" and edit.get("Value"):
                        adjustment_number = edit["Value"]
print(f"Adjustment number: {adjustment_number}")

# --- Read the adjustment back ---
get_payload = {
    "ServiceName": "InventoryAdjustment",
    "TransactionStates": [{
        "DataElementName": "TABPAGE_1.tp_1_dw_1",
        "Keys": [{"Name": "adjustment_number", "Value": adjustment_number}],
    }],
}
resp = httpx.post(f"{ui_server}/api/v2/transaction/get",
                  headers=headers, json=get_payload, verify=False)
resp.raise_for_status()
for txn in resp.json().get("Transactions", []):
    for de in txn.get("DataElements", []):
        for row in de.get("Rows", []):
            for edit in row.get("Edits", []):
                if edit["Name"] in ("adjustment_number", "location_id", "reason_id",
                                    "item_id", "unit_quantity", "new_qoh"):
                    print(f"  {edit['Name']}: {edit['Value']}")
var session = await P21Session.CreateAsync(
    "https://play.p21server.com", "api_user", "api_pass");

var payload = new JObject
{
    ["Name"] = "InventoryAdjustment",
    ["UseCodeValues"] = false,  // reason_id is the display text, not the code
    ["Transactions"] = new JArray
    {
        new JObject
        {
            ["Status"] = "New",
            ["DataElements"] = new JArray
            {
                new JObject
                {
                    ["Name"] = "TABPAGE_1.tp_1_dw_1",
                    ["Type"] = "Form",
                    ["Keys"] = new JArray(),
                    ["Rows"] = new JArray
                    {
                        new JObject
                        {
                            ["Edits"] = new JArray
                            {
                                new JObject { ["Name"] = "company_id", ["Value"] = "ACME" },
                                new JObject { ["Name"] = "location_id", ["Value"] = "10" },
                                new JObject { ["Name"] = "reason_id", ["Value"] = "ADJUST" }, // display text
                                new JObject { ["Name"] = "inv_adj_description", ["Value"] = "Cycle count write-off" }
                            },
                            ["RelativeDateEdits"] = new JArray()
                        }
                    }
                },
                new JObject
                {
                    ["Name"] = "TABPAGE_17.tp_17_dw_17",
                    ["Type"] = "List",
                    ["Keys"] = new JArray(),
                    ["Rows"] = new JArray
                    {
                        new JObject
                        {
                            ["Edits"] = new JArray
                            {
                                new JObject { ["Name"] = "item_id", ["Value"] = "WIDGET-001" },
                                // signed delta, NOT new on-hand
                                new JObject { ["Name"] = "unit_quantity", ["Value"] = "-5" }
                            },
                            ["RelativeDateEdits"] = new JArray()
                        }
                    }
                }
            }
        }
    }
};

var resp = await session.Http.PostAsync(
    $"{session.UiServer}/api/v2/transaction",
    new StringContent(payload.ToString(), Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();  // HTTP 200 even on failure -- check the Summary
var result = JObject.Parse(await resp.Content.ReadAsStringAsync());

var summary = result["Summary"]!;
Console.WriteLine($"Succeeded: {summary["Succeeded"]}, Failed: {summary["Failed"]}");
if ((int)summary["Failed"]! > 0)
{
    foreach (var msg in result["Messages"] as JArray ?? new JArray())
        Console.WriteLine($"  {msg}");
    throw new Exception("Adjustment failed");
}

// Pull the server-generated adjustment_number out of the echoed DataElements
string? adjustmentNumber = null;
foreach (var txn in result["Results"]?["Transactions"] as JArray ?? new JArray())
foreach (var de in txn["DataElements"] as JArray ?? new JArray())
{
    if (de["Name"]?.ToString() != "TABPAGE_1.tp_1_dw_1") continue;
    foreach (var row in de["Rows"] as JArray ?? new JArray())
    foreach (var edit in row["Edits"] as JArray ?? new JArray())
        if (edit["Name"]?.ToString() == "adjustment_number" &&
            !string.IsNullOrEmpty(edit["Value"]?.ToString()))
            adjustmentNumber = edit["Value"]!.ToString();
}
Console.WriteLine($"Adjustment number: {adjustmentNumber}");

// --- Read the adjustment back ---
var getPayload = new JObject
{
    ["ServiceName"] = "InventoryAdjustment",
    ["TransactionStates"] = new JArray
    {
        new JObject
        {
            ["DataElementName"] = "TABPAGE_1.tp_1_dw_1",
            ["Keys"] = new JArray
            {
                new JObject { ["Name"] = "adjustment_number", ["Value"] = adjustmentNumber }
            }
        }
    }
};
var getResp = await session.Http.PostAsync(
    $"{session.UiServer}/api/v2/transaction/get",
    new StringContent(getPayload.ToString(), Encoding.UTF8, "application/json"));
getResp.EnsureSuccessStatusCode();
var getResult = JObject.Parse(await getResp.Content.ReadAsStringAsync());

var wanted = new[] { "adjustment_number", "location_id", "reason_id",
                     "item_id", "unit_quantity", "new_qoh" };
foreach (var txn in getResult["Transactions"] as JArray ?? new JArray())
foreach (var de in txn["DataElements"] as JArray ?? new JArray())
foreach (var row in de["Rows"] as JArray ?? new JArray())
foreach (var edit in row["Edits"] as JArray ?? new JArray())
    if (wanted.Contains(edit["Name"]?.ToString()))
        Console.WriteLine($"  {edit["Name"]}: {edit["Value"]}");

Payload files: JSON · XML — validator-verified, see payloads README.

End-to-end files (runnable from the repo with a .env, dry-run by default): examples/python/recipes/inventory_adjustment.py · examples/csharp/Recipes/InventoryAdjustment.cs. The snippet above is self-contained; the files use the repo's shared common / P21Examples.Common helpers like every other example.

Gotchas

Verify

Read the adjustment back by its key (second half of the example): POST /api/v2/transaction/get on InventoryAdjustment, element TABPAGE_1.tp_1_dw_1, keyed by the server-generated adjustment_number — confirm the header (location_id, reason_id) and the line's item_id / unit_quantity landed, and check new_qoh for the resulting on-hand.

Credit: Alex Westemeier