We have recently been asked by one of our clients to integrate with SAP. This is Enterprise software that helps to manage general business operations.
Where to begin?
The biggest task for this was giving the ability to output an XML for each order. We began by hooking into the normal WooCommerce Order Processing action.
<?php | |
add_action( 'woocommerce_order_status_processing', 'hpy_output_xml', 99, 2); | |
function hpy_output_xml( $order_id, $regenerated = false ) { | |
//As this triggers on the Order Processing action we will have the $order_id handy. First we need to get the Order. | |
$order = new WC_Order( $order_id ); | |
$date = date( "Y_m_d_H_i_s" ); | |
$filename = 'orders-export-' . $date . '.xml'; | |
//This can be changed to alter the location of the output. | |
$filelocation = get_home_path() . 'wp-content/order_xml/'; | |
$file = $filelocation . $filename; | |
//Adding a filter, so this can easily be hooked into and changed later. | |
$xml = apply_filters( 'hpy_xml', $order ); | |
$xml->save( $file, LIBXML_NOEMPTYTAG ); | |
//Update the Order with a note to state when the XML was created. | |
hpy_update_order( $order, $regenerated ); | |
return; | |
} | |
?> |
As you can see above we hooked into ‘woo commerce_order_status_processing’ action. I wanted to make sure that this was the last thing to run, so I had all the information I might need at my disposal.
There are 2 variables being declared, only one of which is being used at the moment, the second was added at a later date (This is explained within the Finishing Touches).
Once you have added the above function we can now begin to set up the XML output. Thankfully we have the filter above. This will allow us to manipulate what is returned.
The nitty gritty of the XML
Below is a complete function that will create the contents of a very small XML. There is minimal output, as the needs for each website will almost definitely be different. I have added comments to the function to explain each step within.
<?php | |
add_filter( 'hpy_xml', 'hpy_format_xml', 10, 1 ); | |
function hpy_format_xml( $order ) { | |
//Get any additional information before we begin creating the XML. | |
$billing_address = $order->get_address( 'billing' ); | |
$shipping_address = $order->get_address( 'shipping' ); | |
//Create a new DOMDocument to handle the XML generation. | |
$xml = new DOMDocument( '1.0', 'UTF-8' ); | |
$xml->formatOutput = true; | |
//Begin the XML with the Element of <orders> | |
$order_xml_outer = $xml->createElement( 'Orders' ); | |
//Within <orders> we want <order> | |
$order_xml = $xml->createElement( 'Order' ); | |
$order_number = get_post_meta( $order->id, '_order_number', true ); | |
if ( !isset( $order_number ) ) { | |
$order_number = ''; | |
} | |
//Now we have all the information we need, begin outputting the XML | |
if ( isset( $order ) ) { | |
$nodes[0] = $xml->createElement( 'OrderId', $order->id ); | |
$nodes[1] = $xml->createElement( 'OrderNumber', $order_number ); | |
$nodes[2] = $xml->createElement( 'OrderDate', $order->order_date ); | |
$nodes[3] = $xml->createElement( 'OrderStatus', $order->get_status() ); | |
} | |
//Billing Details | |
$nodes[4] = $xml->createElement( 'BillingFirstName', htmlspecialchars( $billing_address['first_name'], ENT_XML1, 'UTF-8' ) ); | |
//Repeat for every other element you need. As a WooCommerce store, we use all Billing/Shipping/Payment/Fee information. | |
foreach( $nodes as $node ) { | |
//Once you have all of the required Order information in an array, step through each element and append it as a child to the <order> element. | |
$order_xml->appendChild( $node ); | |
} | |
//As this is for a WooCommerce store, get all the products within the Order. | |
$order_items = $order->get_items(); | |
//Order Lines | |
foreach( $order_items as $item ) { | |
$item_nodes = array(); | |
//Create another Element for the Order Items. | |
$order_lines_xml = $xml->createElement( 'OrderLineItems' ); | |
//Get the product for each Item. | |
$product = wc_get_product( $item['product_id'] ); | |
//Within here you can grab all the information you require. This example, will grab the SKU/Quantity/Price/Name/Total. | |
if( $product->is_type( 'variable' ) ) { | |
$variant = wc_get_product( $item[ 'variation_id' ] ); | |
$sku = $variant->get_sku(); | |
$price = $variant->get_price(); | |
} else { | |
$sku = $product->get_sku(); | |
$price = $product->get_price(); | |
} | |
$item_meta = new WC_Order_Item_Meta( $item ); | |
$item_meta = $item_meta->display( true, true ); | |
//Build an array of the required information for each product. | |
$item_nodes[0] = $xml->createElement( 'SKU', htmlspecialchars( $sku, ENT_XML1, 'UTF-8' ) ); | |
$item_nodes[1] = $xml->createElement( 'Quantity', htmlspecialchars( $item['qty'], ENT_XML1, 'UTF-8' ) ); | |
$item_nodes[2] = $xml->createElement( 'Price', htmlspecialchars( wc_format_decimal( $price, 2 ), ENT_XML1, 'UTF-8' ) ); | |
$item_nodes[3] = $xml->createElement( 'ItemName', htmlspecialchars( html_entity_decode( $product ? $product->get_title() : $item['name'], ENT_NOQUOTES, 'UTF-8' ), ENT_XML1, 'UTF-8' ) ); | |
$item_nodes[4] = $xml->createElement( 'LineTotal', htmlspecialchars( wc_format_decimal( $item['line_total'], 2 ), ENT_XML1, 'UTF-8' ) ); | |
foreach( $item_nodes as $item ) { | |
//Add each item node to the <OrderLineItems> element. | |
$order_lines_xml->appendChild( $item ); | |
} | |
//Once the <OrderLineItems> element contains all the Product information, attach this to the <order> element. | |
$order_xml->appendChild( $order_lines_xml ); | |
} | |
//Add the <order> element into the <orders> element. | |
$order_xml_outer->appendChild( $order_xml ); | |
//Finally, we add everything into the DOMDocument we created earlier and return it back to the WooCommerce Processing hook. | |
$xml->appendChild( $order_xml_outer ); | |
return $xml; | |
} | |
?> |
As you can see from the above function we are passing the order object into our filter. We are using DOMDocument to create the XML as opposed to SimpleXML. Both work nicely, although I personally prefer using DOMDocument as you can have more control over everything with various Node Types as well as being a little more forgiving.
Once you have a DOMDocument set, you can begin adding to it. In the above example I only use 2 different functions to build out the XML. These are $xml->createElement( ‘ElementName’, ‘ElementContents’ ); and $parentVariable->appendChild( $childVariable );. These are both pretty self explanatory, one is used to create a new element, this is essentially a group that will hold all the information that is assigned to it. AppendChild is used to assign specific information to an Element.
Below is the XML outputted from the above example:
<orders> | |
<order> | |
<OrderId>123</OrderId> | |
<OrderNumber>HPY123</OrderNumber> | |
<OrderDate>24/12/2018</OrderDate> | |
<OrderStatus>Processing</OrderStatus> | |
<BillingFirstName>Santa</BillingFirstName> | |
<OrderLineItems> | |
<SKU>SKU010203</SKU> | |
<Quantity>1</Quantity> | |
<Price>$79.95</Price> | |
<ItemName>Sleigh Wax</ItemName> | |
<LineTotal>$79.95</LineTotal> | |
</OrderLineItems> | |
</order> | |
</orders> |
Nice finishing touches.
As this was for a client we added a few finishing touches to it. We added an order note to each order so the client could easily check whether an XML had been generated. We also hooked into the WooCommerce Order Actions and added the ability to regenerate the XML.
<?php | |
//Hook into the WooCommerce Order Actions | |
add_action( 'woocommerce_order_actions', 'hpy_add_order_actions' ); | |
function hpy_add_order_actions( $actions ) { | |
//Adding the Regenerate Order XML action. | |
$actions['hpy_xml_regenerate'] = 'Regenerate order XML'; | |
return $actions; | |
} | |
//Using our new action above, create a function to run when this is selected. | |
add_action( 'woocommerce_order_action_hpy_xml_regenerate', 'hpy_order_action_regenerate_xml' ); | |
function hpy_order_action_regenerate_xml( $order ) { | |
$order_id = $order->id; | |
//Run the Output XML function again, passing true where $regenerated would be. | |
hpy_output_xml( $order_id, true ); | |
return; | |
} | |
//The function that is run to update the order notes, post XML creation. | |
function hpy_update_order( $order, $regenerated ) { | |
//This will almost always be false unless this has been hit after the Regenerate XML action. | |
if ( $regenerated ) { | |
$note = 'Order XML has been regenerated successfully.'; | |
} else { | |
$note = 'Order XML generated and uploaded, Ready for SAP.'; | |
} | |
//Finally, add the correct note to the order. | |
$order->add_order_note( $note ); | |
return; | |
} | |
?> |