Create WooCommerce orders from customer purchase order PDFs: a plugin you can build today
· 5 min read
B2B customers often do not use your WooCommerce checkout. They have a procurement system or a standard template that generates a PDF purchase order, and they email it to you. The person who receives it manually re-types the customer details, finds each product, enters the quantities and prices, and creates the order by hand. For stores that receive a handful of POs a week this is manageable. For stores receiving dozens it is hours of data entry that adds no value.
The existing plugins do not solve this. Purchase Order payment gateway plugins let customers reference a PO number at checkout, but the customer still has to create the order themselves through your site. File upload plugins attach a PDF to an order that already exists. Neither one reads the PDF content and builds the order from it. The poster who asked whether this was genuinely unsolved in the WooCommerce ecosystem had it right.
An AI parsing step changes what a small plugin can do. The plugin described here adds one screen to your WooCommerce admin: paste the text from the PDF, click Extract, and see a review table of the customer details and line items with each item matched to your product catalog. If the data looks right, click Create Order and the pending order appears in WooCommerce. One minute instead of twenty.
What one build gives you
- 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
What it does not do
- 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
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
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.
Why existing purchase order plugins do not help
The WooCommerce ecosystem has several purchase order plugins, but they solve a different problem. A PO gateway plugin adds a payment method at checkout where the customer types a PO number and submits the order themselves. The plugin attaches the PO number to the order record. That works for a self-service B2B portal where your customers are willing to find products on your site and check out. It does not help when a customer emails a PDF they generated in their own procurement software and expects you to process it.
File upload plugins are a step closer: they let your team attach the PDF to an order. But the order still has to exist before the attachment can be added, and the plugin does not read the file. Your team still types the order by hand and then attaches the source document for reference. Useful for record-keeping; it does not save any data-entry time.
The gap is a plugin that starts from the text of a purchase order and ends with a populated WooCommerce order, with a human review step in between. That is the job this build does.
What the plugin extracts and how it matches your products
When you paste PO text and click Extract, the plugin sends the content to the OpenAI API with a structured extraction prompt. The response is a JSON object containing the PO number, the customer name, email, phone, and shipping address, and an array of line items each with the product identifier as it appears on the PO, the quantity, and the unit price.
For each line item, the plugin looks for a WooCommerce product by SKU first. If no SKU match is found, it searches by exact product title. A match shows the product name in the review table. No match shows 'No match: will skip' and logs the item in the order note when the order is created. A PO that refers to your products by a customer-internal part number rather than your SKU will produce no matches, but the review table makes that visible before you commit to anything.
The review step: why you confirm before the order is created
Before any order is created, the plugin shows a review table with every extracted field editable. Customer details are text inputs. Line item quantities and unit prices are number inputs you can correct. This step exists because AI extraction is not perfect: a handwritten annotation on the PO, an unusual address format, or a product name that almost-but-not-quite matches your catalog title can all produce wrong output. The review table is where you catch that before it becomes a WooCommerce order.
For a clean PDF from a repeat customer whose products all have matching SKUs, the review step is a few-second sanity check. For a first-time customer sending an unusual format, it is where you fill in what the AI missed. Either way, the order is not created until you click the button.
When a plugin is not the right tool
If your team receives purchase orders over EDI or as structured XML files, this plugin is not the right approach. EDI integration means connecting to a VAN or a direct FTP/AS2 endpoint and parsing ANSI X12 or EDIFACT segments, which is the territory of specialist middleware, not a WordPress plugin.
If you receive more than a few hundred POs per month, the one-at-a-time workflow will become a bottleneck before the per-PO API cost becomes significant. The natural extension is an email listener that pulls PDF attachments from a dedicated inbox and queues them for import automatically. That is one additional prompt once the core plugin is working.
Questions
- Do I need to copy and paste from the PDF, or can I upload the file?
You paste text. Open the PDF in any viewer (Edge, Chrome, Acrobat, Preview), select all, copy, and paste into the textarea. To handle file uploads with automatic text extraction instead, ask Steem to add a PDF upload input and extract the text client-side using PDF.js before submitting.
- What if the AI extracts the wrong product name or SKU?
The review table shows the extracted identifier next to the matched product before anything is saved. Items with no match show 'No match: will skip'. You can correct quantities and prices in the table, and add unmatched products manually from the order edit screen after creation.
- Does creating the order send a confirmation email to the customer?
No. The order is created in pending status and no email is sent automatically. Change the status to Processing in wp-admin and WooCommerce will send its standard new-order email to the address extracted from the PO.
- How much does the OpenAI API cost per purchase order?
Using gpt-4o-mini, a typical one-page purchase order costs well under a cent per call. At 200 POs per month you would spend a few dollars on API calls at most.
- Our purchase orders arrive as email attachments. Can the plugin pull them automatically?
Not in this build. To add that, ask Steem to add a settings screen for IMAP credentials and a WP-Cron job that checks the inbox, extracts PDF attachments as text, and queues them for import.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
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.
Steem