Create WooCommerce orders from customer purchase order PDFs

Published Updated 3 min read

B2B customers often never touch your checkout. Their procurement system or a standard template produces a PDF purchase order, they email it, and whoever receives it re-types the customer details, finds each product, enters quantities and prices, and builds the order by hand. A handful a week is manageable. Dozens a week is hours of data entry that adds nothing.

Purchase Order gateway plugins let a customer type a PO number at checkout, but the customer still has to place the order on your site. Plugins that add file uploads attach a PDF to an order that already exists. Neither reads the PDF and builds the order from it.

An AI parsing step changes what a small plugin can do. One admin screen: paste the PO text, click Extract, and check a table of customer details and line items matched to your catalogue. If it looks right, click Create Order and a pending order appears in WooCommerce. One minute instead of twenty.

Why existing purchase order plugins do not help

A PO gateway adds a payment method where the customer enters a PO number and submits the order themselves, and the number is stored on the order. That suits a self-service B2B portal. It does nothing for a customer who emails a PDF from their own procurement software and expects you to process it.

Plugins for file uploads get closer: your team can attach the PDF to an order. But the order has to exist first, the plugin never reads the file, and someone still types the order by hand. Good for record-keeping, no help with data entry.

From pasted text to matched products

Open the PDF in any viewer (Edge, Chrome, Acrobat, Preview), select all, copy, and paste into the textarea. Extract sends the text to the OpenAI API, using the key stored on the plugin's settings screen, with a structured extraction prompt. What comes back is JSON: PO number, customer name, email, phone and shipping address, and an array of line items, each with the product identifier as written on the PO, the quantity and the unit price.

Each line is matched to a WooCommerce product by SKU first, then by exact product title. A match shows the product name; no match shows 'No match: will skip' and is listed in the order note when the order is created. A PO that uses the customer's own part numbers instead of your SKUs will match nothing, but you see that in the table before committing.

Nothing is created until you confirm

Every extracted field in the review table is editable: customer details as text, quantities and unit prices as numbers. It is there because extraction is not perfect. A handwritten note on the PO, an unusual address format or a product name that nearly matches your catalogue title can all produce wrong output, and the table is where you catch it.

For a clean PDF from a repeat customer whose products all match by SKU, the review is a few seconds' sanity check. For a first-time customer with an unusual format, it is where you fill in what the extraction missed.

The order is created as pending, with the matched products, billing fields and an order note carrying the PO number and any unmatched items, and no email goes out. Move it to Processing and WooCommerce sends its standard new-order email to the address taken from the PO. Unmatched products can be added on the order screen.

EDI, volume and the inbox

Purchase orders that arrive over EDI or as structured XML are another world: a VAN or a direct FTP/AS2 endpoint, ANSI X12 or EDIFACT segments. That is specialist middleware, not a WordPress plugin.

At more than a few hundred POs a month, the one-at-a-time screen becomes the bottleneck long before the API cost matters. A follow-up message can add an inbox listener: IMAP settings and a WP-Cron job that pulls PDF attachments from a dedicated address and queues them for import.

What you get

Included

  • Extracts PO number, customer name, email, phone, and shipping address from pasted purchase order text using the OpenAI API
  • Matches each PO line item to a WooCommerce product by SKU first, then by exact product title
  • Shows a fully editable review table before creating anything, so AI extraction errors can be corrected
  • Creates a pending WooCommerce order with matched products, billing fields set, and an order note listing the PO number and any unmatched items
  • Settings screen to store your OpenAI API key

Not included

  • PDF file parsing: the plugin works with pasted text; use your PDF viewer's copy-all to extract it first
  • Line items that do not match any product by SKU or title: these are logged in the order note but not added as order lines
  • EDI and XML purchase order formats
  • Multi-currency purchase orders or B2B pricing tiers that differ from your WooCommerce product prices
  • Batch import: the plugin processes one purchase order per session

Questions

Can I upload the PDF instead of pasting text?

Not in this build; it works from pasted text. File upload would need a PDF input with the text extracted in the browser by PDF.js before it is submitted.

How much does the OpenAI API cost per purchase order?

With gpt-4o-mini, a typical one-page purchase order costs well under a cent per call. At 200 POs a month you would spend a few dollars at most.

Build it in Steem

The button opens the builder with this guide's prompt filled in, so you can change anything before it runs. Nothing is built, and nothing is charged, until you send it.

Build this pluginAbout 55 credits. The free plan includes enough for one.
Read the prompt (441 words)

Build a WordPress plugin called WooCommerce PO Import. Plugin Name: WooCommerce PO Import. Description: Paste a purchase order and create a WooCommerce order from it using AI. Single PHP file, no external JavaScript required. Add a submenu page under WooCommerce (capability manage_woocommerce) with menu slug wc-po-import and title 'PO Import'. The page shows two tabs controlled by a URL parameter named tab (default: import). Render tab links as plain anchor tags. Tab 1 - Import: Show a form with method POST containing wp_nonce_field('wc_po_extract'), a textarea name=po_text rows=20 labelled 'Paste purchase order text', and a submit button 'Extract order details'. On POST when the nonce is valid: sanitize po_text. Call wp_remote_post('https://api.openai.com/v1/chat/completions') with headers Authorization: Bearer {get_option('wc_po_import_api_key')}, Content-Type: application/json, body JSON: {"model":"gpt-4o-mini","max_tokens":1000,"messages":[{"role":"system","content":"Extract purchase order data and return only a valid JSON object with these keys: po_number (string), customer (object with keys name, email, phone, address_1, city, state, postcode, country), line_items (array of objects each with keys sku_or_name, quantity, unit_price), notes (string). Use empty string for missing string fields and 0 for missing numbers."},{"role":"user","content":"THE_PO_TEXT"}]}. Parse choices[0].message.content as JSON. For each line_item, run a WP_Query with post_type=product looking for a matching _sku meta value first, then a matching post_title. Store the matched product_id (0 if not found) and product display name. Render a review form with method POST and wp_nonce_field('wc_po_create'): text inputs for po_number and for each customer field (name, email, phone, address_1, city, state, postcode, country). A table with columns: 'From PO' (text input name=item_ref[] pre-filled with sku_or_name), 'Matched product' (product name if matched, else 'No match - will skip'), 'Qty' (number input name=item_qty[]), 'Unit price' (number input name=item_price[]). A hidden input name=item_product_id[] per row. A submit button name=wc_po_action value=create_order labelled 'Create order'. On POST when wc_po_action=create_order and nonce valid: create an order with wc_create_order(). Loop items: for each where item_product_id > 0 call $order->add_product(wc_get_product($id), (int)$qty, ['subtotal'=>(float)$price*(int)$qty,'total'=>(float)$price*(int)$qty]). Set billing fields from customer inputs: split name on first space for first/last name, then set_billing_email(), set_billing_phone(), set_billing_address_1(), set_billing_city(), set_billing_state(), set_billing_postcode(), set_billing_country(). Call $order->calculate_totals(). Collect unmatched item_ref values where product_id was 0. Add order note: 'Imported from PO: ' followed by the PO number, then 'Unmatched items: ' followed by the list or 'none'. Update status to pending with $order->update_status('pending'). Save with $order->save(). Redirect to admin_url('post.php?post='.$order->get_id().'&action=edit'). Tab 2 - Settings: A form with method POST, wp_nonce_field('wc_po_settings'), an input type=text name=wc_po_import_api_key labelled 'OpenAI API key' pre-filled from get_option, and a submit button 'Save'. On POST with valid nonce and manage_woocommerce capability, call update_option with the sanitized value. Show a success admin notice. If the API call returns a WP_Error or the JSON cannot be decoded, show an admin notice with the error message and re-render the paste textarea.