Skip to content

Instantly share code, notes, and snippets.

@brettshumaker
Last active November 22, 2021 04:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brettshumaker/a5203b29c029d96af96f50f26a6b2f30 to your computer and use it in GitHub Desktop.
Save brettshumaker/a5203b29c029d96af96f50f26a6b2f30 to your computer and use it in GitHub Desktop.
Add `woocommerce_shipstation_export_custom_order_xml_data` filter to allow extra data to be sent to ShipStation
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* WC_Shipstation_API_Export Class
*/
class WC_Shipstation_API_Export extends WC_Shipstation_API_Request {
/**
* Constructor
*/
public function __construct() {
if ( ! WC_Shipstation_API::authenticated() ) {
exit;
}
}
/**
* Do the request
*/
public function request() {
global $wpdb;
$this->validate_input( array( 'start_date', 'end_date' ) );
header( 'Content-Type: text/xml' );
$xml = new DOMDocument( '1.0', 'utf-8' );
$xml->formatOutput = true;
$page = max( 1, isset( $_GET['page'] ) ? absint( $_GET['page'] ) : 1 );
$exported = 0;
$tz_offset = get_option( 'gmt_offset' ) * 3600;
$raw_start_date = wc_clean( urldecode( $_GET['start_date'] ) );
$raw_end_date = wc_clean( urldecode( $_GET['end_date'] ) );
// Parse start and end date
if ( $raw_start_date && false === strtotime( $raw_start_date ) ) {
$month = substr( $raw_start_date, 0, 2 );
$day = substr( $raw_start_date, 2, 2 );
$year = substr( $raw_start_date, 4, 4 );
$time = substr( $raw_start_date, 9, 4 );
$start_date = gmdate( 'Y-m-d H:i:s', strtotime( $year . '-' . $month . '-' . $day . ' ' . $time ) );
} else {
$start_date = gmdate( 'Y-m-d H:i:s', strtotime( $raw_start_date ) );
}
if ( $raw_end_date && false === strtotime( $raw_end_date ) ) {
$month = substr( $raw_end_date, 0, 2 );
$day = substr( $raw_end_date, 2, 2 );
$year = substr( $raw_end_date, 4, 4 );
$time = substr( $raw_end_date, 9, 4 );
$end_date = gmdate( 'Y-m-d H:i:s', strtotime( $year . '-' . $month . '-' . $day . ' ' . $time ) );
} else {
$end_date = gmdate( 'Y-m-d H:i:s', strtotime( $raw_end_date ) );
}
if ( version_compare( WC_VERSION, '3.1', '>=' ) ) {
$order_ids = wc_get_orders( array(
'date_modified' => $start_date . '...' . $end_date,
'type' => 'shop_order',
'status' => WC_ShipStation_Integration::$export_statuses,
'return' => 'ids',
'orderby' => 'date_modified',
'order' => 'DESC',
'paged' => $page,
'limit' => WC_SHIPSTATION_EXPORT_LIMIT,
) );
} else {
$order_ids = $order_ids = $wpdb->get_col(
$wpdb->prepare( "
SELECT ID FROM {$wpdb->posts}
WHERE post_type = 'shop_order'
AND post_status IN ( '" . implode( "','", WC_ShipStation_Integration::$export_statuses ) . "' )
AND %s <= post_modified_gmt
AND post_modified_gmt <= %s
ORDER BY post_modified_gmt DESC
LIMIT %d, %d
",
$start_date,
$end_date,
WC_SHIPSTATION_EXPORT_LIMIT * ( $page - 1 ),
WC_SHIPSTATION_EXPORT_LIMIT
)
);
}
// Figure out how to retrieve this using WC Query class.
$max_results = $wpdb->get_var(
$wpdb->prepare( "
SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'shop_order'
AND post_status IN ( '" . implode( "','", WC_ShipStation_Integration::$export_statuses ) . "' )
AND %s <= post_modified_gmt
AND post_modified_gmt <= %s
",
$start_date,
$end_date
)
);
$orders_xml = $xml->createElement( 'Orders' );
foreach ( $order_ids as $order_id ) {
if ( ! apply_filters( 'woocommerce_shipstation_export_order', true, $order_id ) ) {
continue;
}
$order = wc_get_order( $order_id );
$order_xml = $xml->createElement( 'Order' );
$wc_gte_30 = version_compare( WC_VERSION, '3.0', '>=' );// gte greater than or equal to 3.0
$formatted_order_number = ltrim( $order->get_order_number(), '#' );
$this->xml_append( $order_xml, 'OrderNumber', $formatted_order_number );
$this->xml_append( $order_xml, 'OrderID', $order_id );
$order_date = strtotime( $wc_gte_30 ? $order->get_date_created()->date( 'm/d/Y H:i' ) : $order->order_date ) - $tz_offset;
$this->xml_append( $order_xml, 'OrderDate', gmdate( 'm/d/Y H:i', $order_date ), false );
$this->xml_append( $order_xml, 'OrderStatus', $order->get_status() );
$this->xml_append( $order_xml, 'PaymentMethod', $wc_gte_30 ? $order->get_payment_method() : $order->payment_method );
$this->xml_append( $order_xml, 'OrderPaymentMethodTitle', $wc_gte_30 ? $order->get_payment_method_title() : $order->payment_method_title );
$last_modified = strtotime( $wc_gte_30 ? $order->get_date_modified()->date( 'm/d/Y H:i' ) : $order->modified_date ) - $tz_offset;
$this->xml_append( $order_xml, 'LastModified', gmdate( 'm/d/Y H:i', $last_modified ), false );
$this->xml_append( $order_xml, 'ShippingMethod', implode( ' | ', $this->get_shipping_methods( $order ) ) );
$this->xml_append( $order_xml, 'OrderTotal', $order->get_total(), false );
$this->xml_append( $order_xml, 'TaxAmount', wc_round_tax_total( $order->get_total_tax() ), false );
if ( class_exists( 'WC_COG' ) ) {
$this->xml_append( $order_xml, 'CostOfGoods', wc_format_decimal( $order->wc_cog_order_total_cost ), false );
}
$this->xml_append( $order_xml, 'ShippingAmount', $wc_gte_30 ? $order->get_shipping_total() : $order->get_total_shipping(), false );
$this->xml_append( $order_xml, 'CustomerNotes', $wc_gte_30 ? $order->get_customer_note() : $order->customer_note );
$this->xml_append( $order_xml, 'InternalNotes', implode( ' | ', $this->get_order_notes( $order ) ) );
// Custom fields - 1 is used for coupon codes
$this->xml_append( $order_xml, 'CustomField1', implode( ' | ', $order->get_used_coupons() ) );
// Custom fields 2 and 3 can be mapped to a custom field via the following filters
if ( $meta_key = apply_filters( 'woocommerce_shipstation_export_custom_field_2', '' ) ) {
$this->xml_append( $order_xml, 'CustomField2', apply_filters( 'woocommerce_shipstation_export_custom_field_2_value', get_post_meta( $order_id, $meta_key, true ), $order_id ) );
}
if ( $meta_key = apply_filters( 'woocommerce_shipstation_export_custom_field_3', '' ) ) {
$this->xml_append( $order_xml, 'CustomField3', apply_filters( 'woocommerce_shipstation_export_custom_field_3_value', get_post_meta( $order_id, $meta_key, true ), $order_id ) );
}
// Allow users to utilize additional ShipStation fields
if ( has_filter( 'woocommerce_shipstation_export_custom_order_xml' ) ) {
// Set blank defaults
$defaults = array(
'field' => '',
'value' => '',
'cdata' => false,
);
// Developers can hook in here to add more fields
// Unfortunately, you will need to hook in for each field you want to add
$custom_data = apply_filters( 'woocommerce_shipstation_export_custom_order_xml', $defaults, $order_id );
// Ensure we get back an array from the filter
if ( is_array( $custom_data ) ) {
$field = isset( $custom_data['field'] ) && '' == $custom_data['field'] ? $defaults['field'] : $custom_data['field'];
$value = isset( $custom_data['value'] ) && '' == $custom_data['value'] ? $defaults['value'] : $custom_data['value'];
$cdata = isset( $custom_data['cdata'] ) && '' == $custom_data['cdata'] ? $defaults['cdata'] : $custom_data['cdata'];
if ( '' !== $field && '' !== $value )
$this->xml_append( $order_xml, $field, $value, $cdata );
} else {
trigger_error( 'The filter "woocommerce_shipstation_export_custom_order_xml" should return an array.' );
}
}
// Customer data
$customer_xml = $xml->createElement( 'Customer' );
$this->xml_append( $customer_xml, 'CustomerCode', $wc_gte_30 ? $order->get_billing_email() : $order->billing_email );
$billto_xml = $xml->createElement( 'BillTo' );
$this->xml_append( $billto_xml, 'Name', ( $wc_gte_30 ? $order->get_billing_first_name() : $order->billing_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_billing_last_name() : $order->billing_last_name ) );
$this->xml_append( $billto_xml, 'Company', $wc_gte_30 ? $order->get_billing_company() : $order->billing_company );
$this->xml_append( $billto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
$this->xml_append( $billto_xml, 'Email', $wc_gte_30 ? $order->get_billing_email() : $order->billing_email );
$customer_xml->appendChild( $billto_xml );
$shipto_xml = $xml->createElement( 'ShipTo' );
$shipping_country = $wc_gte_30 ? $order->get_shipping_country() : $order->shipping_country;
if ( empty( $shipping_country ) ) {
$name = ( $wc_gte_30 ? $order->get_billing_first_name() : $order->billing_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_billing_last_name() : $order->billing_last_name );
$this->xml_append( $shipto_xml, 'Name', $name );
$this->xml_append( $shipto_xml, 'Company', $wc_gte_30 ? $order->get_billing_company() : $order->billing_company );
$this->xml_append( $shipto_xml, 'Address1', $wc_gte_30 ? $order->get_billing_address_1() : $order->billing_address_1 );
$this->xml_append( $shipto_xml, 'Address2', $wc_gte_30 ? $order->get_billing_address_2() : $order->billing_address_2 );
$this->xml_append( $shipto_xml, 'City', $wc_gte_30 ? $order->get_billing_city() : $order->billing_city );
$this->xml_append( $shipto_xml, 'State', $wc_gte_30 ? $order->get_billing_state() : $order->billing_state );
$this->xml_append( $shipto_xml, 'PostalCode', $wc_gte_30 ? $order->get_billing_postcode() : $order->billing_postcode );
$this->xml_append( $shipto_xml, 'Country', $wc_gte_30 ? $order->get_billing_country() : $order->billing_country );
$this->xml_append( $shipto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
} else {
$name = ( $wc_gte_30 ? $order->get_shipping_first_name() : $order->shipping_first_name ) . ' ' . ( $wc_gte_30 ? $order->get_shipping_last_name() : $order->shipping_last_name );
$this->xml_append( $shipto_xml, 'Name', $name );
$this->xml_append( $shipto_xml, 'Company', $wc_gte_30 ? $order->get_shipping_company() : $order->shipping_company );
$this->xml_append( $shipto_xml, 'Address1', $wc_gte_30 ? $order->get_shipping_address_1() : $order->shipping_address_1 );
$this->xml_append( $shipto_xml, 'Address2', $wc_gte_30 ? $order->get_shipping_address_2() : $order->shipping_address_2 );
$this->xml_append( $shipto_xml, 'City', $wc_gte_30 ? $order->get_shipping_city() : $order->shipping_city );
$this->xml_append( $shipto_xml, 'State', $wc_gte_30 ? $order->get_shipping_state() : $order->shipping_state );
$this->xml_append( $shipto_xml, 'PostalCode', $wc_gte_30 ? $order->get_shipping_postcode() : $order->shipping_postcode );
$this->xml_append( $shipto_xml, 'Country', $wc_gte_30 ? $order->get_shipping_country() : $order->shipping_country );
$this->xml_append( $shipto_xml, 'Phone', $wc_gte_30 ? $order->get_billing_phone() : $order->billing_phone );
}
$customer_xml->appendChild( $shipto_xml );
$order_xml->appendChild( $customer_xml );
// Item data
$found_item = false;
$items_xml = $xml->createElement( 'Items' );
// Merge arrays without loosing indexes.
$order_items = $order->get_items() + $order->get_items( 'fee' );
foreach ( $order_items as $item_id => $item ) {
if ( $wc_gte_30 ) {
$product = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : false;
} else {
$product = $order->get_product_from_item( $item );
}
$item_needs_no_shipping = ! $product || ! $product->needs_shipping();
$item_not_a_fee = 'fee' !== $item['type'];
if ( $item_needs_no_shipping && $item_not_a_fee ) {
continue;
}
$found_item = true;
$item_xml = $xml->createElement( 'Item' );
$this->xml_append( $item_xml, 'LineItemID', $item_id );
if ( 'fee' === $item['type'] ) {
$this->xml_append( $item_xml, 'Name', $item['name'] );
$this->xml_append( $item_xml, 'Quantity', 1, false );
$this->xml_append( $item_xml, 'UnitPrice', $order->get_item_total( $item, false, true ), false );
}
// handle product specific data
if ( $product && $product->needs_shipping() ) {
$this->xml_append( $item_xml, 'SKU', $product->get_sku() );
$this->xml_append( $item_xml, 'Name', $product->get_title() );
// image data
$image_id = $product->get_image_id();
$image_url = $image_id ? current( wp_get_attachment_image_src( $image_id, 'shop_thumbnail' ) ) : '';
$this->xml_append( $item_xml, 'ImageUrl', $image_url );
$this->xml_append( $item_xml, 'Weight', wc_get_weight( $product->get_weight(), 'oz' ), false );
$this->xml_append( $item_xml, 'WeightUnits', 'Ounces', false );
$this->xml_append( $item_xml, 'Quantity', $item['qty'], false );
$this->xml_append( $item_xml, 'UnitPrice', $order->get_item_subtotal( $item, false, true ), false );
}
if ( $item['item_meta'] ) {
if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
$item_meta = new WC_Order_Item_Meta( $item, $product );
$formatted_meta = $item_meta->get_formatted( '_' );
} else {
add_filter( 'woocommerce_is_attribute_in_product_name', '__return_false' );
$formatted_meta = $item->get_formatted_meta_data();
}
if ( ! empty( $formatted_meta ) ) {
$options_xml = $xml->createElement( 'Options' );
foreach ( $formatted_meta as $meta_key => $meta ) {
$option_xml = $xml->createElement( 'Option' );
if ( version_compare( WC_VERSION, '3.0.0', '<' ) ) {
$this->xml_append( $option_xml, 'Name', $meta['label'] );
$this->xml_append( $option_xml, 'Value', $meta['value'] );
} else {
$this->xml_append( $option_xml, 'Name', $meta->display_key );
$this->xml_append( $option_xml, 'Value', wp_strip_all_tags( $meta->display_value ) );
}
$options_xml->appendChild( $option_xml );
}
$item_xml->appendChild( $options_xml );
}
}
$items_xml->appendChild( $item_xml );
}
if ( ! $found_item ) {
continue;
}
// Append cart level discount line
if ( $order->get_total_discount() ) {
$item_xml = $xml->createElement( 'Item' );
$this->xml_append( $item_xml, 'SKU', 'total-discount' );
$this->xml_append( $item_xml, 'Name', __( 'Total Discount', 'woocommerce-shipstation' ) );
$this->xml_append( $item_xml, 'Adjustment', 'true', false );
$this->xml_append( $item_xml, 'Quantity', 1, false );
$this->xml_append( $item_xml, 'UnitPrice', $order->get_total_discount() * -1, false );
$items_xml->appendChild( $item_xml );
}
// Append items XML
$order_xml->appendChild( $items_xml );
$orders_xml->appendChild( $order_xml );
$exported ++;
// Add order note to indicate it has been exported to Shipstation.
if ( 'yes' !== get_post_meta( $order_id, '_shipstation_exported', true ) ) {
$order->add_order_note( __( 'Order has been exported to Shipstation', 'woocommerce-shipstation' ) );
update_post_meta( $order_id, '_shipstation_exported', 'yes' );
}
}
$orders_xml->setAttribute( 'page', $page );
$orders_xml->setAttribute( 'pages', ceil( $max_results / WC_SHIPSTATION_EXPORT_LIMIT ) );
$xml->appendChild( $orders_xml );
echo $xml->saveXML();
$this->log( sprintf( __( 'Exported %s orders', 'woocommerce-shipstation' ), $exported ) );
}
/**
* Get shipping method names
* @param WC_Order $order
* @return array
*/
private function get_shipping_methods( $order ) {
$shipping_methods = $order->get_shipping_methods();
$shipping_method_names = array();
foreach ( $shipping_methods as $shipping_method ) {
// Replace non-AlNum characters with space
$method_name = preg_replace( '/[^A-Za-z0-9 \-\.\_,]/', '', $shipping_method['name'] );
$shipping_method_names[] = $method_name;
}
return $shipping_method_names;
}
/**
* Get Order Notes
* @param WC_Order $order
* @return array
*/
private function get_order_notes( $order ) {
$args = array(
'post_id' => version_compare( WC_VERSION, '3.0.0', '>=' ) ? $order->get_id() : $order->id,
'approve' => 'approve',
'type' => 'order_note',
);
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$order_notes = array();
foreach ( $notes as $note ) {
if ( 'WooCommerce' !== $note->comment_author ) {
$order_notes[] = $note->comment_content;
}
}
return $order_notes;
}
/**
* Append XML as cdata
*/
private function xml_append( $append_to, $name, $value, $cdata = true ) {
$data = $append_to->appendChild( $append_to->ownerDocument->createElement( $name ) );
if ( $cdata ) {
$data->appendChild( $append_to->ownerDocument->createCDATASection( $value ) );
} else {
$data->appendChild( $append_to->ownerDocument->createTextNode( $value ) );
}
}
}
return new WC_Shipstation_API_Export();
<?php
/**
* Here's an example of how to use the woocommerce_shipstation_export_custom_order_xml filter
*
* ShipStation Order XML structure can be found here: https://help.shipstation.com/hc/en-us/articles/205928478-ShipStation-Custom-Store-Development-Guide
*/
// Maybe set the "Gift" field status
add_filter( 'woocommerce_shipstation_export_custom_order_xml', 'maybe_set_gift_status', 10, 2 );
function maybe_set_gift_status( $defaults, $order_id ) {
// Look at our custom fields to see if this order is a gift
// Also check to see if the gift message is empty, if so, don't send
$is_gift = get_post_meta( $order_id, '_checkout_shipping_gift', true );
$gift_message = get_post_meta( $order_id, '_checkout_gift_message', true );
if ( 1 == $is_gift && '' !== $gift_message) {
// Tell ShipStation this is a gift
$defaults['field'] = 'Gift';
$defaults['value'] = 'true';
}
return $defaults;
}
add_filter( 'woocommerce_shipstation_export_custom_order_xml', 'maybe_set_gift_message', 10, 2 );
function maybe_set_gift_message( $defaults, $order_id ) {
// Look at our custom fields to see if this order is a gift
// Also check to see if the gift message is empty, if so, don't send
$is_gift = get_post_meta( $order_id, '_checkout_shipping_gift', true );
$gift_message = get_post_meta( $order_id, '_checkout_gift_message', true );
if ( 1 == $is_gift && '' !== $gift_message) {
// Add the gift message data for ShipStation
$defaults['field'] = 'GiftMessage';
$defaults['value'] = $gift_message;
$defaults['cdata'] = true;
}
return $defaults;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment