Generate Pick Ticket and PO PDFs

Generate or reprint pick tickets and purchase orders as base64-encoded PDFs via the dedicated report endpoint.

API: Transaction (POST /api/v2/process/pdfreport) · Service: m_picktickets, m_reprintpicktickets, m_reprintpurchaseorders · Deep dive: PDF Report Generation · Full schema: m_picktickets.json · m_reprintpicktickets.json · m_reprintpurchaseorders.json

Prerequisites

Wrong-endpoint trap: POST /api/v2/transaction accepts an m_* report payload and returns Succeeded — but emits nothing. A report is a process, not a record edit; it must go to POST /api/v2/process/pdfreport.

Payload

Constants for every report payload: Status and Type are numeric 0 (not the "New" record-edit shape) and the DataElement carries Keys: []. Only Name, the DataElement name, and the criteria Edits change per service.

m_pickticketscreates the pick-ticket record at location_id and returns its PDF in one call (verified worked example). Requires UseCodeValues: true with the code "P" (Production Order):

POST {ui_server}/api/v2/process/pdfreport

{
  "Name": "m_picktickets",
  "UseCodeValues": true,
  "Transactions": [{
    "Status": 0,
    "DataElements": [{
      "Keys": [],
      "Type": 0,
      "Name": "TABPAGE_1.tp_1_dw_1",
      "Rows": [{ "Edits": [
        { "Name": "create_pick_ticket_type", "Value": "P" },
        { "Name": "beg_prod_order", "Value": "1000123" },
        { "Name": "end_prod_order", "Value": "1000123" },
        { "Name": "location_id",    "Value": "10" }
      ] }]
    }]
  }]
}

m_reprintpurchaseorders — PO reprint (verified with UseCodeValues: false). DataElement name is TABPAGE_1.poreportcriteriadw:

{ "Name": "company_id", "Value": "ACME" },
{ "Name": "beg_po_no",  "Value": "500100" },
{ "Name": "end_po_no",  "Value": "500100" },
{ "Name": "reprint_flag", "Value": "Y" }

m_reprintpicktickets — pick-ticket reprint. DataElement name is TABPAGE_1.tp_1_dw_1 (datawindow d_reprint_pick_ticket_criteria); the definition marks company_id, location_id, and print_qty required, with ranges beg_pick_ticket_no/end_pick_ticket_no (sales-order tickets) and beg_prod_pick_ticket_no/end_prod_pick_ticket_no (production tickets).

For any other report, swap Name and the criteria Edits (field names from GET /api/v2/definition/{name}); the endpoint, Status/Type: 0, Keys: [], and the DocumentData extraction stay the same.

Complete example

Generates a production-order pick ticket with m_picktickets, decodes the base64 PDF, and handles both failure shapes (document-level ResponseStatus and the P21 error envelope).

import base64

import httpx  # p21_auth() from recipes/README.md

BASE_URL = "https://play.p21server.com"
USERNAME = "api_user"
PASSWORD = "api_pass"

token, ui_server, headers = p21_auth(BASE_URL, USERNAME, PASSWORD)

payload = {
    "Name": "m_picktickets",
    "UseCodeValues": True,   # required here -- False returns HTTP 500
    "Transactions": [{
        "Status": 0,         # numeric 0 for report payloads
        "DataElements": [{
            "Keys": [],      # always empty for reports
            "Type": 0,       # numeric 0 for report payloads
            "Name": "TABPAGE_1.tp_1_dw_1",
            "Rows": [{"Edits": [
                # code "P" = Production Order (the display label is rejected)
                {"Name": "create_pick_ticket_type", "Value": "P"},
                {"Name": "beg_prod_order", "Value": "1000123"},
                {"Name": "end_prod_order", "Value": "1000123"},
                # location whose inventory the components pick from
                {"Name": "location_id", "Value": "10"},
            ]}],
        }],
    }],
}

response = httpx.post(
    f"{ui_server}/api/v2/process/pdfreport",
    headers=headers, json=payload, verify=False, timeout=120,
)

# Errors come back as the standard P21 error envelope (ErrorType/ErrorMessage),
# NOT the Summary/Messages format used by /transaction.
if response.status_code >= 400:
    raise SystemExit(f"HTTP {response.status_code}: {response.text}")
result = response.json()
if isinstance(result, dict) and "ErrorMessage" in result:
    raise SystemExit(f"{result.get('ErrorType')}: {result['ErrorMessage']}")

# Success is a JSON ARRAY -- even for a single document
if not (isinstance(result, list) and result):
    raise SystemExit(f"No documents returned: {result}")

for doc in result:
    status = doc.get("ResponseStatus", {}).get("StatusCode")
    if status != "Success" or not doc.get("DocumentData"):
        msg = doc.get("ResponseStatus", {}).get("Message", "Unknown error")
        print(f"Document failed: {msg}")
        continue
    pdf_bytes = base64.b64decode(doc["DocumentData"])
    # FileName includes .pdf, e.g. "PPT<nnn> PRODUCTION_PICK_TICKET.pdf"
    filename = doc.get("FileName", "pick_ticket.pdf")
    with open(filename, "wb") as f:
        f.write(pdf_bytes)
    print(f"Saved {filename} ({len(pdf_bytes)} bytes)")
var session = await P21Session.CreateAsync(
    "https://play.p21server.com", "api_user", "api_pass");

var payload = new JObject
{
    ["Name"] = "m_picktickets",
    ["UseCodeValues"] = true,   // required here -- false returns HTTP 500
    ["Transactions"] = new JArray
    {
        new JObject
        {
            ["Status"] = 0,     // numeric 0 for report payloads
            ["DataElements"] = new JArray
            {
                new JObject
                {
                    ["Keys"] = new JArray(),  // always empty for reports
                    ["Type"] = 0,             // numeric 0 for report payloads
                    ["Name"] = "TABPAGE_1.tp_1_dw_1",
                    ["Rows"] = new JArray
                    {
                        new JObject
                        {
                            ["Edits"] = new JArray
                            {
                                // code "P" = Production Order (label is rejected)
                                new JObject { ["Name"] = "create_pick_ticket_type", ["Value"] = "P" },
                                new JObject { ["Name"] = "beg_prod_order", ["Value"] = "1000123" },
                                new JObject { ["Name"] = "end_prod_order", ["Value"] = "1000123" },
                                // location whose inventory the components pick from
                                new JObject { ["Name"] = "location_id", ["Value"] = "10" },
                            }
                        }
                    }
                }
            }
        }
    }
};

var response = await session.Http.PostAsync(
    $"{session.UiServer}/api/v2/process/pdfreport",
    new StringContent(payload.ToString(), Encoding.UTF8, "application/json"));
var bodyText = await response.Content.ReadAsStringAsync();

// Errors come back as the standard P21 error envelope (ErrorType/ErrorMessage),
// NOT the Summary/Messages format used by /transaction.
if (!response.IsSuccessStatusCode)
    throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {bodyText}");

var parsed = JToken.Parse(bodyText);
if (parsed is JObject envelope && envelope["ErrorMessage"] != null)
    throw new InvalidOperationException(
        $"{envelope["ErrorType"]}: {envelope["ErrorMessage"]}");

// Success is a JSON ARRAY -- even for a single document
if (parsed is not JArray documents || documents.Count == 0)
    throw new InvalidOperationException($"No documents returned: {parsed}");

foreach (var doc in documents.OfType<JObject>())
{
    var status = doc["ResponseStatus"]?["StatusCode"]?.ToString();
    var documentData = doc["DocumentData"]?.ToString();
    if (status != "Success" || string.IsNullOrEmpty(documentData))
    {
        var msg = doc["ResponseStatus"]?["Message"]?.ToString() ?? "Unknown error";
        Console.WriteLine($"Document failed: {msg}");
        continue;
    }
    var pdfBytes = Convert.FromBase64String(documentData);
    // FileName includes .pdf, e.g. "PPT<nnn> PRODUCTION_PICK_TICKET.pdf"
    var filename = doc["FileName"]?.ToString() ?? "pick_ticket.pdf";
    await File.WriteAllBytesAsync(filename, pdfBytes);
    Console.WriteLine($"Saved {filename} ({pdfBytes.Length} bytes)");
}

Payload files: JSON · reprint JSON (XML untested for the pdfreport endpoint) — validator-verified, see payloads README.

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

Gotchas

All verified live — details in PDF Report Generation:

Verify

Credit: Alex Westemeier — worked example verified end-to-end (report run → pick ticket row created → PDF returned → ticket confirmed and completed). Jeff Poss discovered the /api/v2/process/pdfreport endpoint.