hundred/ for Sage 100

Guide ยท Sage 100 purchasing and stock

Sage 100 purchase orders, receipts of goods and inventory by warehouse

Checked against Sage 100 file layouts, the Sage 100 object reference and Sage community answers on 2026-10-17.

Short answer: create the purchase order with the Business Object Interface (BOI) object PO_PurchaseOrder_bus, enter the receipt with PO_Receipt_bus (receipt type G, goods), and post it with PO_ReceiptRegister_upd. The stock does not move when the receipt is entered: according to Sage community answers, nothing reaches inventory until the receipt register is updated. Read the result over ODBC: IM_ItemWarehouse has QuantityOnHand and QuantityOnPurchaseOrder for each item and warehouse. BOI is a Windows COM interface, so all the write steps run on a Windows machine with Sage 100 installed, signed in as a Sage 100 user.

Which object or table for each step

StepThroughObject or table
Create a purchase order (Purchase Order Entry)BOIPO_PurchaseOrder_bus, task PO_PurchaseOrder_ui
Enter a receipt of goods (Receipt of Goods Entry)BOIPO_Receipt_bus with ReceiptType G, task PO_ReceiptOfGoods_ui
Post the receipt (Daily Receipt Registers/Update)BOIPO_ReceiptRegister_upd, task PO_ReceiptRegister_ui
Read stock per warehouseODBCIM_ItemWarehouse, names from IM_Warehouse
Read open purchase order linesODBCPO_PurchaseOrderHeader, PO_PurchaseOrderDetail
Read posted receiptsODBCPO_ReceiptHistoryHeader, PO_ReceiptHistoryDetail

The ODBC driver for Sage 100 Standard and Advanced is read-only, so every write goes through BOI (see the ODBC guide). Writing to the data files, or to the SQL tables on Premium, skips Sage's validation and is not supported by Sage. If you only need a nightly file import of purchase orders, Visual Integrator, a separately licensed Sage module, may be enough; the BOI guide compares the two.

What BOI needs before you write a line

  • A Windows machine with the Sage 100 workstation installed, because BOI is a COM interface (ProvideX.Script). Your process must match the bitness of the installed Sage 100; Sage 100 2026 is 64-bit only (what breaks in 2026).
  • A Sage 100 user whose role allows each task you call: Purchase Order Entry, Receipt of Goods Entry and the receipt register update. The session checks the task you set with nSetProgram.
  • A license seat. Plan for a connected BOI session to take a user license seat while it is open; older community answers say so, so confirm it for your version with your reseller. Drop the objects and call nCleanup when you finish.
  • External access for the company. When a BOI program could not open the company's files, the verified answer on the Sage community was to check the company's Allow External Access setting.
  • The Purchase Order module date. Set the session to module P/O with a date; the community examples pass dates as YYYYMMDD ("20261017"), both to nSetDate and to date fields such as PurchaseOrderDate$.
  • Python needs one extra step. ProvideX objects give pywin32 no type information. The verified community answer wraps each object with DumbDispatch and names its methods with _FlagAsMethod; the examples below do that.
  • Your own testing. Sage's BOI FAQ says Sage "cannot troubleshoot, debug, or assist with the design of programs not written by Sage developers". Run everything against a copy of the company first.

Locks: what your program has to expect

  • Records in use. Sage 100 tracks whether a purchase order is in use: the purchase order object carries a read-only OrderInUse$ property. When Sage refuses to open a record, nSetKey can return 0 with the reason in sLastErrorMsg (a community thread shows this for a sales order that was being shipped). Treat it as "try again later", not as a crash.
  • Batches in use. If batch entry is on for receipts, a batch can be in use by someone else; a community thread checks for that before writing. Give your program its own batch number so it never shares one with your staff.
  • The register. The update refuses to run until the register has been printed ("The register must be printed prior to posting"), so the program has to print or export it first, as shown below.

Step 1: create a purchase order

A standard order for 24 of item 1001-HON-H252 into warehouse 000 from vendor 01-CONTAINER, the same data as the purchase order example in Hundred's planned API reference. The pattern follows the verified Python and purchase order answers on the Sage community, listed under Sources. Test it against a copy of the company before you point it at live data.

import pythoncom
import win32com.client as win32

HOME = r"C:\Sage\Sage 100 Standard\MAS90\Home"
TODAY = "20261017"                                   # YYYYMMDD

def com(obj, *methods):
    """ProvideX objects have no type info for Python: name the methods."""
    o = win32.dynamic.DumbDispatch(obj)
    for m in methods + ("DropObject",):
        o._FlagAsMethod(m)
    return o

def need(ret, obj, wanted=1):
    if ret < wanted:
        raise RuntimeError(obj.sLastErrorMsg)

def out():
    """A string Sage can write back into, such as the next order number."""
    return win32.VARIANT(pythoncom.VT_BYREF | pythoncom.VT_BSTR, "")

px = win32.dynamic.Dispatch("ProvideX.Script")
px.Init(HOME)
ss = com(px.NewObject("SY_Session"), "nLogon", "nSetUser", "nSetCompany",
         "nSetDate", "nSetModule", "nSetProgram", "nLookupTask", "nCleanup")
if ss.nLogon() == 0:
    need(ss.nSetUser("APIUSER", "secret"), ss)
need(ss.nSetCompany("ABC"), ss)
need(ss.nSetDate("P/O", TODAY), ss)
need(ss.nSetModule("P/O"), ss)
need(ss.nSetProgram(ss.nLookupTask("PO_PurchaseOrder_ui")), ss)

po = com(px.NewObject("PO_PurchaseOrder_bus", ss),
         "nGetNextPurchaseOrderNo", "nSetKey", "nSetValue", "nWrite")
lines = com(po.oLines, "nAddLine", "nSetValue", "nWrite")
try:
    po_no = out()
    need(po.nGetNextPurchaseOrderNo(po_no), po)
    need(po.nSetKey(po_no.value), po, wanted=2)     # 2 = a new order
    need(po.nSetValue("APDivisionNo$", "01"), po)
    need(po.nSetValue("VendorNo$", "CONTAINER"), po)
    need(po.nSetValue("PurchaseOrderDate$", TODAY), po)

    need(lines.nAddLine(), lines)
    need(lines.nSetValue("ItemCode$", "1001-HON-H252"), lines)
    need(lines.nSetValue("WarehouseCode$", "000"), lines)
    need(lines.nSetValue("QuantityOrdered", 24), lines)
    need(lines.nSetValue("UnitCost", 71.20), lines)
    need(lines.nWrite(), lines)

    need(po.nWrite(), po)
    print("Purchase order", po_no.value)
finally:
    po.DropObject()
FieldTableWhat to know
PurchaseOrderNo (7)Header keyTake the next number from nGetNextPurchaseOrderNo, or set your own. nSetKey returns 2 when it starts a new order; a lower value means it did not, so check whether the number is taken.
APDivisionNo (2) + VendorNo (7)HeaderThe vendor. A community poster's order came out wrong with ARDivisionNo; it must be the A/P division.
PurchaseOrderDateHeaderA string field: PurchaseOrderDate$, YYYYMMDD.
OrderTypeHeaderS standard (the default), D drop ship, M master, R repeating, X material requisition.
WarehouseCode (3)Header and lineEach line carries its own warehouse. Set it on every line if you buy for more than one warehouse.
ItemCode (30), QuantityOrdered, UnitCostLineSet UnitCost when you agreed a price; otherwise check what Sage fills in on your first test orders.
RequiredDate, UnitOfMeasure (4)LineThe date and unit your buyers expect. Read them back with the line.

String fields take a $ suffix in nSetValue ("VendorNo$"), numbers do not ("QuantityOrdered"). Lines are written first, the header last: nothing is saved until the header's nWrite succeeds. To make a retry safe, keep your own reference next to the Sage order number before you write, and look it up before you create the order again.

Step 2: enter the receipt of goods

Same session. Switch the task, create the receipt, point it at the purchase order and copy the lines. nCopyPurchaseOrderLines(1) receives every line complete, as the Receive Complete check box does on screen; 0 copies the lines without received quantities, so you set them yourself.

BATCH = "00042"   # your program's own batch; leave it out if batch entry is off

need(ss.nSetProgram(ss.nLookupTask("PO_ReceiptOfGoods_ui")), ss)
rg = com(px.NewObject("PO_Receipt_bus", ss), "nSelectBatch", "nGetNextReceiptNo",
         "nSetKeyValue", "nSetKey", "nSetValue", "nCopyPurchaseOrderLines", "nWrite")
try:
    need(rg.nSelectBatch(BATCH), rg)
    need(rg.nSetKeyValue("ReceiptType$", "G"), rg)      # G = goods
    receipt_no = out()
    need(rg.nGetNextReceiptNo(receipt_no), rg)
    need(rg.nSetKeyValue("ReceiptNo$", receipt_no.value), rg)
    need(rg.nSetKey(), rg, wanted=2)
    need(rg.nSetValue("ReceiptDate$", TODAY), rg)
    need(rg.nSetValue("PurchaseOrderNo$", po_no.value), rg)
    need(rg.nCopyPurchaseOrderLines(1), rg)           # 1 = receive complete
    need(rg.nWrite(), rg)
    print("Receipt", receipt_no.value, "in batch", BATCH)
finally:
    rg.DropObject()

Partial receipts. In place of the nCopyPurchaseOrderLines(1) line, copy with 0, walk the lines and set what arrived, then write the receipt as above:

received = {"1001-HON-H252": 20}                    # item code -> quantity in the box

need(rg.nCopyPurchaseOrderLines(0), rg)
lines = com(rg.oLines, "nMoveFirst", "nMoveNext", "nEOF",
            "nGetValue", "nSetValue", "nWrite")
lines.nMoveFirst()
while lines.nEOF() == 0:
    item = out()
    lines.nGetValue("ItemCode$", item)
    if item.value in received:
        need(lines.nSetValue("QuantityReceived", received[item.value]), lines)
        need(lines.nWrite(), lines)
    lines.nMoveNext()
  • Over-receiving fails unless your Purchase Order options allow a received quantity greater than the ordered one; a community poster hit exactly that error. Check the option before you code around it.
  • The rest of the order. What you do not receive stays on back order on the purchase order. Whether to clear it is a buying decision, not a coding one; Sage community members warn that a vendor may still ship the balance.
  • Still nothing in stock. The receipt now sits in Receipt of Goods Entry. On-hand quantities change at the register update in step 3.

Step 3: post the receipt (register update)

The receipt of goods register is the same update your staff run from Daily Receipt Registers/Update. Through BOI it is PO_ReceiptRegister_upd: select the batch if batch entry is on, set the posting date, print or export the register, then update.

need(ss.nSetProgram(ss.nLookupTask("PO_ReceiptRegister_ui")), ss)
reg = com(px.NewObject("PO_ReceiptRegister_upd", ss),
          "nSelectBatch", "nSetPostingDate", "nProcessReport", "nUpdate")
try:
    if reg.nBatchEnabled == 1:
        need(reg.nSelectBatch(BATCH), reg)
    need(reg.nSetPostingDate(TODAY), reg)
    need(reg.nProcessReport("DEFERRED"), reg)   # or "Print" / "Export"
    need(reg.nUpdate(), reg)
finally:
    reg.DropObject()
    ss.nCleanup()
    ss.DropObject()
  • The register must be printed first. "DEFERRED" sends it to Sage's deferred printing queue. "Print" needs Paperless Office or a default printer for the Windows user the program runs as.
  • Without batches, the update is not limited to your receipts. If batch entry is off there is no batch to select, so test on a copy what the update picks up. If both your program and your staff enter receipts, turn batch entry on and give the program its own batch.
  • When nProcessReport fails with no message, look at the Sage 100 workstation files on that machine first: in the community thread the cause was files missing from the workstation setup (a configuration file), not the code.

Step 4: read on hand and on order per warehouse (ODBC)

Connect as in the ODBC guide (or with hundred-odbc in Python). Three queries cover the purchasing side.

Stock per warehouse for one item

SELECT w.ItemCode, w.WarehouseCode, h.WarehouseDesc,
       w.QuantityOnHand, w.QuantityOnPurchaseOrder,
       w.QuantityOnSalesOrder, w.QuantityOnBackOrder
FROM IM_ItemWarehouse w, IM_Warehouse h
WHERE w.WarehouseCode = h.WarehouseCode
  AND w.ItemCode = '1001-HON-H252'

ItemCode + WarehouseCode is the key of IM_ItemWarehouse, so this is fast. The table has no "available" column: decide your own formula, for example on hand minus on sales order, and write it down so every system uses the same one.

Open purchase order lines for one item, per warehouse

SELECT h.PurchaseOrderNo, h.APDivisionNo, h.VendorNo, h.OrderStatus,
       d.WarehouseCode, d.RequiredDate, d.QuantityOrdered,
       d.QuantityReceived, d.QuantityBackordered
FROM PO_PurchaseOrderHeader h, PO_PurchaseOrderDetail d
WHERE h.PurchaseOrderNo = d.PurchaseOrderNo
  AND d.ItemCode = '1001-HON-H252'
  AND h.OrderType = 'S'
  AND h.OrderStatus IN ('N', 'O', 'B', 'C')

OrderStatus is N new, O open, B back ordered, C change or X complete. The detail table's key is PurchaseOrderNo + LineKey, so a filter on ItemCode reads the whole file: run this on a schedule, not on every web request. Subtract QuantityReceived from QuantityOrdered in your code to get what is still to come, line by line, with its required date.

Posted receipts for one purchase order

SELECT h.ReceiptNo, h.ReceiptDate, d.ItemCode, d.WarehouseCode,
       d.QuantityReceived, d.UnitCost
FROM PO_ReceiptHistoryHeader h, PO_ReceiptHistoryDetail d
WHERE h.PurchaseOrderNo = d.PurchaseOrderNo
  AND h.ReceiptType = d.ReceiptType
  AND h.ReceiptNo = d.ReceiptNo
  AND h.HeaderSeqNo = d.HeaderSeqNo
  AND h.PurchaseOrderNo = '0000351'
  AND h.ReceiptType = 'G'

Receipts that are entered but not yet updated are in PO_ReceiptHeader and PO_ReceiptDetail (receipt type G) instead.

Check the whole flow on a copy of the company: run the stock query before step 1, after step 1, after step 2 and after step 3, and note which column moves when. That tells you, for your version and your options, when QuantityOnPurchaseOrder rises and falls and when QuantityOnHand changes, before you build a sync on top of it.

The same with Hundred (planned, not live)

Hundred is a planned REST API for Sage 100. It is not built yet; nothing below is callable today, and the base URL does not answer. This is what the planned API reference lists for this guide's steps:

Step in this guidePlanned endpointStatus in the reference
Create a purchase orderPOST /purchase-ordersListed. The request body is not published yet.
Read a purchase order and what was receivedGET /purchase-orders/{purchaseOrderNo}Listed: one order with lines and received quantities.
List purchase ordersGET /purchase-ordersListed, with filters status, vendorId and updatedSince.
Stock per warehouseGET /items/{itemCode}/stockListed: on hand, on sales order and on purchase order, per warehouse.
Enter or post a receipt of goodsNoneNot in the reference. Until it is, receipts stay with Receipt of Goods Entry or a BOI program like the one above.
GET /v1/companies/ABC/purchase-orders/0000351      (planned)

{
  "purchaseOrderNo": "0000351", "vendorId": "01-CONTAINER", "status": "open",
  "lines": [ { "itemCode": "1001-HON-H252", "quantityOrdered": 24, "quantityReceived": 0, "unitCost": 71.20 } ]
}

As planned, writes would go through the same Sage 100 business objects as above, run by a small Hundred connector on the Sage 100 server (or a workstation with the Sage 100 client installed), so the requirements in this guide (Windows, a Sage 100 user, a license seat while connected) would still apply on that machine.

Before you build

  1. Create a dedicated Sage 100 user for the program, with a role limited to Purchase Order Entry, Receipt of Goods Entry and the receipt register.
  2. Turn on batch entry for receipts if people and a program both enter them, and give the program its own batch.
  3. Read your Purchase Order options: over-receiving, closing partly received orders and the register printing all change what the code has to do.
  4. Make every write safe to repeat: store your reference and the Sage number together, and look before you create.
  5. Check the version. Sage 100 2026 is 64-bit only; the program must be 64-bit too.

Sources: Sage 100 file layouts for PO_PurchaseOrderHeader, PO_PurchaseOrderDetail, PO_ReceiptHeader, PO_ReceiptDetail, PO_ReceiptHistoryHeader, PO_ReceiptHistoryDetail, IM_ItemWarehouse and IM_Warehouse; Sage 100 object reference for PO_PurchaseOrder_Bus, PO_Receipt_Bus and PO_ReceiptRegister_upd; Sage 100 help, Enter a receipt of goods and the BOI FAQs; Sage community threads on creating a purchase order, the A/P division, receipt of goods objects, receiving part of a line, back orders after a receipt, when inventory is updated, updating the receipt register, BOI from Python, external access and a record held elsewhere; pywin32, win32com.client.VARIANT.

Hundred is a planned REST API for Sage 100: purchase orders and per-warehouse stock over one HTTPS endpoint, with a small connector on the Sage 100 server running BOI and ODBC for you. Receipts of goods are not in its reference yet. It is not built yet. Early access, rolling out in stages.

Join early access