Purchase Orders | Set Purchase Order

Purchase Order Creation and Update Endpoint

This document provides guidance on using the API to create or update purchase orders in the API server's database through the set_purchase_order endpoint. This involves two steps: API authentication and purchase order creation/update.

API ENDPOINT FOR PURCHASE ORDER CREATION AND UPDATE

This API call is essential for creating new purchase orders or updating existing ones in the system.

Purchase Order Creation/Update Process

To create or update a purchase order, follow the steps below using the set_purchase_order API call. The process involves providing credentials for API login and specific details for the purchase order.

Endpoint and Method

  • Endpoint: https://easycms.fi/public_api/set_purchase_order/
  • Method: POST
  • Content-Type: application/json

Parameters | Payload

For purchase order operations, you need to provide two sets of parameters - one for API authentication and the other for the purchase order details.

API Authentication Parameters

  • TOKEN (api_key): Your unique API key for authentication. How to Create API Credentials.
  • username: Your login username.
  • password: Your login password.
  • account: Your specific account ID.

Purchase Order Parameters

For CREATE (id NOT provided):
  • supplier_id: Supplier ID (Integer) - Required. The ID of the supplier for this purchase order.
  • order_date: Order Date (String) - The date of the order placement. Defaults to current date if not provided.
  • delivery_date: Delivery Date (String) - The expected delivery date.
  • status: Status (Integer) - The status of the purchase order. Defaults to 1 (draft) if not provided.
  • purchase_order_lines: Array of purchase order line data - Required for creation.
  • Other optional purchase order fields listed below.
For UPDATE (id provided):
  • id: Purchase Order ID (Integer) - Required for update. The ID of the purchase order to update.
  • Any purchase order fields to update (only provided fields will be updated).
  • purchase_order_lines: Array of purchase order line data (optional for update).

Purchase Order Header Fields

  • id: Purchase Order ID (Integer) - The unique identifier for the purchase order (for updates only).
  • reference: Reference Number (Integer) - The reference number for the purchase order (auto-generated for new orders).
  • supplier_id: Supplier ID (Integer) - The ID of the supplier for this purchase order.
  • title: Title (String) - The title of the purchase order.
  • note: Note (String) - General notes for the order.
  • admin_note: Admin Note (String) - Administrative notes.
  • delivery_instructions: Delivery Instructions (String) - Special delivery instructions.
  • order_personnel_name: Order Personnel Name (String) - Name of the person who placed the order.
  • loading_personnel_name: Loading Personnel Name (String) - Name of the loading personnel.
  • delivery_personnel_name: Delivery Personnel Name (String) - Name of the delivery personnel.
  • recipient_personnel_name: Recipient Personnel Name (String) - Name of the recipient.
  • payer_details: Payer Details (String) - Information about the payer.
  • payee_details: Payee Details (String) - Information about the payee.
  • reason: Reason (String) - The reason for the purchase order.
  • total: Total (Float) - The total amount of the purchase order.
  • discount: Discount (Float) - Any discount applied to the order.
  • status: Status (Integer) - The status of the purchase order:
    • 1 = Draft
    • 100 = Sent to supplier
    • 110 = Preparing
    • 120 = Shipped
    • 200 = Received
    • -1 = Cancelled

Purchase Order Lines Fields

Each item in the purchase_order_lines array can contain:

Field Type Required Description
pid Integer Yes Product ID
quantity Float Yes Ordered quantity. Read-only when status = 200 (received)
unit_price Float Yes Purchase price per unit
received_quantity Float No Actually received quantity. Only applicable when status = 200
arrival_date String No Arrival date of goods (format: YYYY-MM-DD)
product_name String No Product name override
line_note String No Notes specific to this line
best_before_date or bbd String No Best before date. Accepted formats: YYYY-MM-DD (recommended), DD.MM.YYYY, MM/DD/YYYY, ISO-8601. Normalized to a date literal before storing (v3.16). Send it before confirming the line — the confirm step copies the line's bbd into the stock records
barcode String No Product barcode
boxcode String No Box code
location_id String No Storage location ID
shelf_id Integer No Shelf identifier
discount Float No Discount percentage


Status-Based Quantity Rules

The API enforces business rules matching the admin CMS behavior for quantity fields:

Order Status quantity (Ordered) received_quantity (Received)
Draft (1), Sent (100), Preparing (110), Shipped (120) Editable - set the qty you plan to order Ignored (set to 0)
Received (200) READ-ONLY - API returns error if you try to change it Editable - set the actual qty received
Cancelled (-1) Read-only Read-only

Important: When updating lines on a received order (status = 200):

  • You MUST provide the original quantity value (it must match the existing ordered qty)
  • You can update received_quantity to reflect what was actually received
  • Attempting to change quantity on a received order will result in an error

Confirmed-line safety rules (v3.16+): re-sending purchase_order_lines on a received (status 200) order follows these rules, so duplicate/idempotent submissions are safe:

  • Already-confirmed line re-sent unchanged → SKIPPED (idempotent no-op; reported in lines_skipped_confirmed). The line's confirmed stock is never touched by a re-send.
  • Already-confirmed line with a NEW received_quantity → the previous confirmed stock movement is reversed, the line is un-confirmed and staged with the new quantity — call set_purchase_order_confirm again to apply the corrected amount.
  • Stop re-sending a line once it has been confirmed (set_purchase_order_confirm); send only the lines you are still working on.


Two-Phase Receive Confirmation (v3.08+)

When a purchase order's status is changed to 200 (received):

  • Stock is NO LONGER automatically moved on the status change
  • All lines are set to received_quantity = quantity (pre-filled)
  • Each line must be individually confirmed via set_purchase_order_confirm to move stock
  • Alternatively, use set_purchase_order_confirm without purchase_line_id to confirm all lines at once

This ensures accurate stock tracking -- the person receiving goods explicitly confirms what was actually received.


Call Examples in Different Languages


# Create a new purchase order (draft)
curl -X POST 'https://easycms.fi/public_api/set_purchase_order' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "supplier_id": 5,
  "order_date": "2024-01-15 10:30:00",
  "status": 1,
  "title": "Monthly Restock Order",
  "note": "Regular monthly order",
  "delivery_instructions": "Deliver to warehouse B",
  "purchase_order_lines": [
    {
      "pid": 12345,
      "quantity": 100,
      "unit_price": 15.50,
      "product_name": "Product Name",
      "barcode": "1234567890123",
      "bbd": "2025-12-31"
    },
    {
      "pid": 12346,
      "quantity": 50,
      "unit_price": 22.00,
      "product_name": "Another Product",
      "boxcode": "BOX001"
    }
  ]
}'

# Update a received order with received_quantity
# Note: quantity must match the original ordered qty (read-only when received)
curl -X POST 'https://easycms.fi/public_api/set_purchase_order' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "id": 123,
  "status": 200,
  "note": "Order received and verified",
  "purchase_order_lines": [
    {
      "pid": 12345,
      "quantity": 100,
      "unit_price": 15.50,
      "received_quantity": 95,
      "arrival_date": "2024-01-19"
    }
  ]
}'

# Update order header only (no lines)
curl -X POST 'https://easycms.fi/public_api/set_purchase_order' \
-H 'Authorization1: TOKEN' \
-H 'Content-Type: application/json' \
-d '{
  "username": "USERNAME",
  "password": "PASSWORD",
  "account": "ACCOUNT_ID",
  "id": 123,
  "status": 200,
  "note": "Order received and verified"
}'

$curl = curl_init();

$payload = [
  'username' => 'USERNAME',
  'password' => 'PASSWORD',
  'account' => 'ACCOUNT_ID',
  'supplier_id' => 5,
  'order_date' => '2024-01-15 10:30:00',
  'status' => 1,
  'title' => 'Monthly Restock Order',
  'note' => 'Regular monthly order',
  'delivery_instructions' => 'Deliver to warehouse B',
  'purchase_order_lines' => [
    [
      'pid' => 12345,
      'quantity' => 100,
      'unit_price' => 15.50,
      'product_name' => 'Product Name',
      'barcode' => '1234567890123',
      'bbd' => '2025-12-31'
    ],
    [
      'pid' => 12346,
      'quantity' => 50,
      'unit_price' => 22.00,
      'product_name' => 'Another Product',
      'boxcode' => 'BOX001'
    ]
  ]
];

curl_setopt_array($curl, [
  CURLOPT_URL => "https://easycms.fi/public_api/set_purchase_order",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode($payload),
  CURLOPT_HTTPHEADER => [
    "Authorization1: TOKEN",
    "Content-Type: application/json"
  ],
]);

$response = curl_exec($curl);
curl_close($curl);
echo $response;

import requests
import json

url = "https://easycms.fi/public_api/set_purchase_order"
headers = {
    "Authorization1": "TOKEN",
    "Content-Type": "application/json"
}

# Create a new purchase order
payload = {
    "username": "USERNAME",
    "password": "PASSWORD",
    "account": "ACCOUNT_ID",
    "supplier_id": 5,
    "order_date": "2024-01-15 10:30:00",
    "status": 1,
    "title": "Monthly Restock Order",
    "note": "Regular monthly order",
    "delivery_instructions": "Deliver to warehouse B",
    "purchase_order_lines": [
        {
            "pid": 12345,
            "quantity": 100,
            "unit_price": 15.50,
            "product_name": "Product Name",
            "barcode": "1234567890123",
            "bbd": "2025-12-31"
        },
        {
            "pid": 12346,
            "quantity": 50,
            "unit_price": 22.00,
            "product_name": "Another Product",
            "boxcode": "BOX001"
        }
    ]
}

response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.text)

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        String jsonPayload = """
            {
              "username": "USERNAME",
              "password": "PASSWORD",
              "account": "ACCOUNT_ID",
              "supplier_id": 5,
              "order_date": "2024-01-15 10:30:00",
              "status": 1,
              "title": "Monthly Restock Order",
              "note": "Regular monthly order",
              "purchase_order_lines": [
                {
                  "pid": 12345,
                  "quantity": 100,
                  "unit_price": 15.50,
                  "barcode": "1234567890123"
                }
              ]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://easycms.fi/public_api/set_purchase_order"))
            .headers("Authorization1", "TOKEN", "Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
            .build();

        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.body());
    }
}

const https = require('https');

const payload = {
  username: 'USERNAME',
  password: 'PASSWORD',
  account: 'ACCOUNT_ID',
  supplier_id: 5,
  order_date: '2024-01-15 10:30:00',
  status: 1,
  title: 'Monthly Restock Order',
  note: 'Regular monthly order',
  purchase_order_lines: [
    {
      pid: 12345,
      quantity: 100,
      unit_price: 15.50,
      product_name: 'Product Name',
      barcode: '1234567890123',
      bbd: '2025-12-31'
    }
  ]
};

const data = JSON.stringify(payload);

const options = {
  hostname: 'easycms.fi',
  path: '/public_api/set_purchase_order',
  method: 'POST',
  headers: {
    'Authorization1': 'TOKEN',
    'Content-Type': 'application/json',
    'Content-Length': data.length
  }
};

const req = https.request(options, (res) => {
  let responseData = '';
  res.on('data', (chunk) => { responseData += chunk; });
  res.on('end', () => { console.log(responseData); });
});

req.on('error', (e) => { console.error(e); });
req.write(data);
req.end();

import React, { useEffect, useState } from 'react';

function App() {
  const [responseData, setResponseData] = useState('');

  useEffect(() => {
    const createPurchaseOrder = async () => {
      try {
        const response = await fetch('https://easycms.fi/public_api/set_purchase_order', {
          method: 'POST',
          headers: {
            'Authorization1': 'TOKEN',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            username: 'USERNAME',
            password: 'PASSWORD',
            account: 'ACCOUNT_ID',
            supplier_id: 5,
            order_date: '2024-01-15 10:30:00',
            status: 1,
            title: 'Monthly Restock Order',
            note: 'Regular monthly order',
            purchase_order_lines: [
              {
                pid: 12345,
                quantity: 100,
                unit_price: 15.50,
                barcode: '1234567890123'
              }
            ]
          })
        });
        const data = await response.json();
        setResponseData(JSON.stringify(data, null, 2));
      } catch (error) {
        console.error(error);
      }
    };
    createPurchaseOrder();
  }, []);

  return <pre>{responseData}</pre>;
}

export default App;

import okhttp3.OkHttpClient
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException

fun main() {
    val client = OkHttpClient()

    val jsonPayload = """
        {
          "username": "USERNAME",
          "password": "PASSWORD",
          "account": "ACCOUNT_ID",
          "supplier_id": 5,
          "order_date": "2024-01-15 10:30:00",
          "status": 1,
          "title": "Monthly Restock Order",
          "note": "Regular monthly order",
          "purchase_order_lines": [
            {
              "pid": 12345,
              "quantity": 100,
              "unit_price": 15.50,
              "barcode": "1234567890123"
            }
          ]
        }
    """.trimIndent()

    val requestBody = jsonPayload.toRequestBody("application/json".toMediaType())

    val request = Request.Builder()
        .url("https://easycms.fi/public_api/set_purchase_order")
        .post(requestBody)
        .addHeader("Authorization1", "TOKEN")
        .build()

    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) throw IOException("Unexpected code $response")
        println(response.body?.string())
    }
}

using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Add("Authorization1", "TOKEN");

        var payload = new
        {
            username = "USERNAME",
            password = "PASSWORD",
            account = "ACCOUNT_ID",
            supplier_id = 5,
            order_date = "2024-01-15 10:30:00",
            status = 1,
            title = "Monthly Restock Order",
            note = "Regular monthly order",
            purchase_order_lines = new[]
            {
                new
                {
                    pid = 12345,
                    quantity = 100,
                    unit_price = 15.50,
                    barcode = "1234567890123"
                }
            }
        };

        var jsonContent = new StringContent(
            JsonSerializer.Serialize(payload),
            Encoding.UTF8,
            "application/json"
        );

        var response = await httpClient.PostAsync(
            "https://easycms.fi/public_api/set_purchase_order",
            jsonContent
        );

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine(responseBody);
        }
        else
        {
            Console.WriteLine($"Error: {response.StatusCode}");
        }
    }
}




Handling Endpoint Results

When you make a request to the endpoint, you receive a JSON response containing various keys and values. Here's an explanation of the response keys and their meanings:


Response Structure:

- `status`: Response status - "success" or "error"
- `message`: Human-readable message describing the result
- `data`: Object containing the created/updated order data
  - `id`: The purchase order ID
  - `order`: The complete order object with all fields
- `changes`: Object tracking what was changed
  - For CREATE:
    - `order_created`: Object with new order details (id, reference, supplier_id)
    - `order_lines`: Result of line processing
  - For UPDATE:
    - `order`: Object with field-level changes (old and new values)
    - `order_lines`: Result of line processing (if lines were provided)

Order Object Fields:
- `id`: Purchase Order ID - Unique identifier
- `reference`: Reference Number - Auto-generated reference
- `supplier_id`: Supplier ID - The supplier for this order
- `order_date`: Order Date - When the order was placed
- `status`: Status Code - Current order status
- `title`: Title - Order title
- `note`: Note - General notes
- `admin_note`: Admin Note - Administrative notes
- `delivery_instructions`: Delivery Instructions
- `total`: Total - Order total amount
- `discount`: Discount - Applied discount
- `payer_details`: Payer Details
- `payee_details`: Payee Details
- `create_date`: Create Date - When record was created
- `update_date`: Update Date - When record was last updated

Order Lines Result:
- `success`: Boolean - Whether line processing succeeded (true when at least one line was processed OR skipped as already-confirmed)
- `lines_processed`: Integer - Number of lines successfully processed
- `lines_skipped`: Integer - Number of lines skipped due to errors
- `lines_skipped_confirmed`: Integer - Number of already-confirmed lines skipped (idempotent re-send, no changes made)
- `errors`: Array of Strings - Error descriptions for skipped lines
- `notes`: Array of Strings - Per-line informational notes (e.g. confirmed quantity reversed and un-confirmed)
- `lines`: Array of Objects - Per-line breakdown (one entry per processed line)
  - `index`: The line's position in the request array
  - `pid`: Product ID of the line
  - `purchase_line_id`: The line's `purchase_line_id` — use this to reference the line in `set_purchase_order_line` (inline editing) or in later `purchase_order_lines` updates
  - `action`: `"added"` (new line created) or `"updated"` (existing line updated)
  - `number`: Line number assigned within the order
- `message`: String - Summary of the result
    

{
  "status": "success",
  "message": "Purchase order created successfully",
  "data": {
    "id": 124,
    "order": {
      "id": "124",
      "reference": "2024001",
      "supplier_id": "5",
      "order_date": "2024-01-15 10:30:00",
      "status": "1",
      "title": "Monthly Restock Order",
      "note": "Regular monthly order",
      "admin_note": "",
      "delivery_instructions": "Deliver to warehouse B",
      "total": "0.0000",
      "discount": "0.0000",
      "create_date": "2024-01-15 10:30:00",
      "update_date": "2024-01-15 10:30:00",
      "deleted": "0"
    }
  },
  "changes": {
    "order_created": {
      "id": 124,
      "reference": "2024001",
      "supplier_id": 5
    },
    "order_lines": {
      "success": true,
      "lines_processed": 2,
      "lines_skipped": 0,
      "errors": [],
      "lines": [
        { "index": 0, "pid": 101, "purchase_line_id": 551, "action": "added", "number": 1 },
        { "index": 1, "pid": 102, "purchase_line_id": 552, "action": "added", "number": 2 }
      ],
      "message": "Purchase order lines updated for 2 product(s)"
    }
  }
}
    

// Example: Marking order as received with actual received quantities
// POST with id=123, status=200, and purchase_order_lines with received_quantity

{
  "status": "success",
  "message": "Purchase order updated successfully",
  "data": {
    "id": 123,
    "order": {
      "id": "123",
      "reference": "2024000",
      "supplier_id": "5",
      "order_date": "2024-01-14 09:00:00",
      "status": "200",
      "title": "Monthly Restock Order",
      "note": "Order received and verified",
      "total": "1550.0000",
      "discount": "0.0000",
      "create_date": "2024-01-14 09:00:00",
      "update_date": "2024-01-15 14:30:00",
      "deleted": "0"
    }
  },
  "changes": {
    "order": {
      "status": {
        "old": "120",
        "new": 200
      },
      "note": {
        "old": "Regular monthly order",
        "new": "Order received and verified"
      }
    },
    "order_lines": {
      "success": true,
      "lines_processed": 2,
      "lines_skipped": 0,
      "errors": [],
      "lines": [
        { "index": 0, "pid": 101, "purchase_line_id": 551, "action": "added", "number": 1 },
        { "index": 1, "pid": 102, "purchase_line_id": 552, "action": "added", "number": 2 }
      ],
      "message": "Purchase order lines updated for 2 product(s)"
    }
  }
}
    

// Example: Attempting to change ordered quantity on a received order
// The API blocks this because quantity is read-only when status=200

{
  "status": "success",
  "message": "Purchase order updated successfully",
  "data": {
    "id": 123,
    "order": { ... }
  },
  "changes": {
    "order": {
      "note": {
        "old": "Old note",
        "new": "Updated note"
      }
    },
    "order_lines": {
      "success": true,
      "lines_processed": 1,
      "lines_skipped": 1,
      "errors": [
        "Line 1: Cannot change ordered quantity (quantity) for PID 12345 - order status is 'received' (200). Ordered quantity is read-only once received. Use received_quantity to update received amounts."
      ],
      "message": "Purchase order lines updated for 1 product(s). Skipped 1 line(s)."
    }
  }
}
    

Purchase Order Status Flow

Purchase orders follow a status flow from creation to completion:

Status Code Title Description
1 draft_order Order is in draft state, can be modified freely
100 order_sent_to_supplier Order has been sent to supplier
110 supplier_preparing_for_shipment Supplier is preparing the order
120 supplier_shipped Order has been shipped by supplier
200 order_received Order received - quantity becomes read-only, received_quantity is editable
-1 cancelled Order has been cancelled

Important status rules:

  • When status changes to 200 (received), the quantity field on each line becomes read-only
  • The received_quantity field is used to record the actual amount received per line
  • The system uses received_quantity for totals calculation when status is 200

Error Handling

Here are the possible error messages and their meanings:

  • UN-AUTHORIZED - _user_name_password_is_set_but_wrong_value!: Incorrect username or password.
  • this_account_does_not_exist_or_your_credentials_do_not_match_this_account: The account doesn't exist or mismatched credentials.
  • UN-AUTHORIZED - header is set but the header value is not correct!: Incorrect authorization header value.
  • METHOD_NOT_ALLOWED: Only POST method is allowed for this endpoint.
  • MISSING_REQUIRED_FIELDS: Missing required fields for creation. Required: supplier_id
  • MISSING_ORDER_LINES: Purchase order lines are required for creating a purchase order.
  • ORDER_NOT_FOUND: The specified purchase order ID was not found (for updates).
  • UPDATE_FAILED: Failed to update the purchase order in the database.
  • CREATE_FAILED: Failed to create the purchase order in the database.

Line-level errors (returned in changes.order_lines.errors array):

  • Cannot change ordered quantity (quantity) for PID {pid} - order status is 'received' (200): You attempted to change the ordered quantity on a received order. Ordered qty is read-only once received. Use received_quantity instead.
  • Missing required fields (pid, quantity, unit_price): A line item is missing one of the required fields.
  • Product PID {pid} not found: The specified product ID does not exist.

Best Practices

  1. Creating a Purchase Order: Always provide supplier_id and purchase_order_lines when creating a new order.
  2. Updating a Purchase Order: Only include fields that need to be changed. The API uses partial updates.
  3. Status Management: Update status progressively through the order lifecycle (1 → 100 → 110 → 120 → 200).
  4. Receiving Goods: When marking an order as received (status=200), provide received_quantity for each line to record actual amounts. The quantity field must remain unchanged.
  5. Line Items: Each line requires pid, quantity, and unit_price at minimum.
  6. Quantity vs Received Quantity:
    • Use quantity for the amount you ordered (editable before receiving)
    • Use received_quantity for the amount you actually received (editable only after receiving)
    • These may differ (e.g., ordered 100, received 95 due to damage/shortage)
  7. Additional Fields: Use bbd (or best_before_date), barcode, boxcode, and arrival_date to capture additional product information during receiving.
  8. Do not re-send confirmed lines: Once a line is confirmed (set_purchase_order_confirm), stop including it in subsequent purchase_order_lines arrays. It is ignored (idempotent skip), but sending only pending lines keeps responses clean and avoids ambiguity.