403 ) ); }
check_admin_referer( self::ACTION );
$raw = isset( $_GET['order_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_GET['order_ids'] ) ) : array();
$ids = array_values( array_unique( array_filter( $raw ) ) );
if ( ! $ids || count( $ids ) > 100 ) { wp_die( esc_html( I18n::t( 'Select 1–100 orders.' ) ), 400 ); }
$prices = ! empty( $_GET['prices'] );
// A selection with nothing to print (every order closed) says so instead of failing (review 0.59).
try { $body = self::body( $ids, $prices ); } catch ( \Throwable $e ) { wp_die( esc_html( $e->getMessage() ), '', array( 'response'=>400, 'back_link'=>true ) ); }
nocache_headers();
header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
self::render( 'storesuit-packing-slips', $prices ? I18n::t( 'Order summaries' ) : I18n::t( 'Packing slips' ), self::CSS, $body );
exit;
}
/**
* Prints one of StoreRuler's print pages (packing slips, order summaries, the pick list) as a complete document. Its style
* and its Print button's script go out through WordPress (the inline style of a registered style handle, an inline script
* tag); the body, built from escaped values, goes out through wp_kses_post().
*/
public static function render( string $handle, string $title, string $css, string $body ): void {
if ( ! wp_style_is( $handle, 'registered' ) ) {
wp_register_style( $handle, false, array(), STORESUIT_VERSION );
wp_add_inline_style( $handle, $css );
}
echo '
' . esc_html( $title ) . "\n";
wp_styles()->do_item( $handle );
echo '';
echo wp_kses_post( $body );
wp_print_inline_script_tag( 'document.querySelectorAll(".ss-print").forEach(function(button){button.addEventListener("click",function(){window.print();});});', array( 'id'=>'storesuit-print-js' ) );
echo '';
}
/** A complete print document as a string (render()'s output). */
public static function capture( string $handle, string $title, string $css, string $body ): string {
$level = ob_get_level();
ob_start();
try { self::render( $handle, $title, $css, $body ); return (string) ob_get_contents(); }
finally { while ( ob_get_level() > $level ) { ob_end_clean(); } }
}
/**
* The pages' contents: per order, its number, date, address lines, delivery and lines (name, options, SKU, units).
* With prices (an order summary): every order but a draft, the units as ordered with the refunded ones, the line
* prices and WooCommerce's totals rows (as HTML it formats, kept to safe tags), the billing details and the status.
*/
public static function build( array $ids, bool $prices = false ): array {
Store::require_cap( 'storesuit_orders' );
$slips = array(); $skipped = array();
foreach ( array_slice( array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ), 0, 100 ) as $id ) {
$order = wc_get_order( $id );
if ( ! $order instanceof \WC_Order || $order->get_type() !== 'shop_order' ) { continue; }
if ( $order->has_status( $prices ? array( 'trash', 'checkout-draft' ) : array( 'cancelled', 'failed', 'refunded', 'trash', 'checkout-draft' ) ) || SaleStatus::quote( $order ) ) { $skipped[] = (string) $order->get_order_number(); continue; }
$lines = array();
foreach ( $order->get_items() as $item_id => $item ) {
if ( ! $item instanceof \WC_Order_Item_Product ) { continue; }
$refunded = abs( (float) $order->get_qty_refunded_for_item( $item_id ) );
$qty = $prices ? (float) $item->get_quantity() : (float) $item->get_quantity() - $refunded;
if ( $qty <= 0 ) { continue; }
$product = $item->get_product(); $what = array();
foreach ( $item->get_formatted_meta_data( '_', true ) as $meta ) { $what[] = wp_strip_all_tags( (string) $meta->display_key ) . ': ' . wp_strip_all_tags( (string) $meta->display_value ); }
$line = array( 'name'=>$item->get_name(), 'what'=>implode( ', ', $what ), 'sku'=>$product ? (string) $product->get_sku() : '', 'qty'=>$qty );
if ( $prices ) { $line['refunded'] = $refunded; $line['price'] = wp_kses_post( (string) $order->get_formatted_line_subtotal( $item ) ); }
$lines[] = $line;
}
$pickup = $order->get_meta( '_storesuit_delivery' ) === 'pickup';
$type = $order->has_shipping_address() ? 'shipping' : 'billing';
$address = $pickup ? '' : trim( wp_strip_all_tags( (string) WC()->countries->get_formatted_address( $order->get_address( $type ), "\n" ) ) );
$slip = array(
'number'=>(string) $order->get_order_number(), 'date'=>$order->get_date_created() ? wc_format_datetime( $order->get_date_created() ) : '',
'pickup'=>$pickup, 'address'=>$address, 'phone'=>(string) ( $order->get_shipping_phone() ?: $order->get_billing_phone() ),
'method'=>$pickup ? '' : trim( (string) $order->get_shipping_method() ), 'note'=>trim( (string) $order->get_customer_note() ), 'lines'=>$lines,
);
// More notes for the box (StoreRuler Pro's gift wrap and gift note): {title, text} pairs, printed as plain text.
$slip['notes'] = array();
foreach ( (array) apply_filters( 'storesuit_packing_slip_notes', array(), $order ) as $extra ) { if ( is_array( $extra ) && is_string( $extra['title'] ?? null ) && is_string( $extra['text'] ?? null ) && $extra['text'] !== '' ) { $slip['notes'][] = array( 'title'=>$extra['title'], 'text'=>$extra['text'] ); } }
if ( $prices ) {
$slip['status'] = wc_get_order_status_name( $order->get_status() );
$slip['billing'] = trim( wp_strip_all_tags( (string) WC()->countries->get_formatted_address( $order->get_address( 'billing' ), "\n" ) ) );
$slip['email'] = (string) $order->get_billing_email();
$slip['totals'] = array();
foreach ( (array) $order->get_order_item_totals() as $row ) { $slip['totals'][] = array( 'label'=>wp_kses_post( (string) ( $row['label'] ?? '' ) ), 'value'=>wp_kses_post( (string) ( $row['value'] ?? '' ) ) ); }
}
$slips[] = $slip;
}
return array( 'slips'=>$slips, 'skipped'=>$skipped );
}
/** The store's name and address, as WooCommerce's settings keep them. */
private static function store(): array {
$address = WC()->countries->get_formatted_address( array( 'company'=>get_bloginfo( 'name' ), 'address_1'=>WC()->countries->get_base_address(), 'address_2'=>WC()->countries->get_base_address_2(), 'city'=>WC()->countries->get_base_city(), 'state'=>WC()->countries->get_base_state(), 'postcode'=>WC()->countries->get_base_postcode(), 'country'=>WC()->countries->get_base_country() ), "\n" );
return array( 'address'=>trim( wp_strip_all_tags( (string) $address ) ) );
}
/** The complete printed document of the selected orders (what print_page() prints). */
public static function document( array $ids, bool $prices = false ): string {
return self::capture( 'storesuit-packing-slips', $prices ? I18n::t( 'Order summaries' ) : I18n::t( 'Packing slips' ), self::CSS, self::body( $ids, $prices ) );
}
/** The document's body: the toolbar with its Print button, one page per order, the orders left out. */
private static function body( array $ids, bool $prices ): string {
$data = self::build( $ids, $prices );
if ( ! $data['slips'] ) { throw new \InvalidArgumentException( esc_html( I18n::t( 'No orders found to print.' ) ) ); }
$store = self::store(); $pages = '';
foreach ( $data['slips'] as $slip ) {
$rows = '';
foreach ( $slip['lines'] as $line ) {
$rows .= '| ' . esc_html( $line['name'] ) . '' . ( $line['what'] !== '' ? '' . esc_html( $line['what'] ) . '' : '' ) . ( ! empty( $line['refunded'] ) ? '' . esc_html( I18n::format( 'Refunded: %s', wc_stock_amount( $line['refunded'] ) ) ) . '' : '' ) . ' | ' . esc_html( $line['sku'] ) . ' | ' . esc_html( wc_stock_amount( $line['qty'] ) ) . ' | ' . ( $prices ? '' . $line['price'] . ' | ' : '' ) . '
';
}
$totals = '';
foreach ( $slip['totals'] ?? array() as $row ) { $totals .= '| ' . $row['label'] . ' | ' . $row['value'] . ' |
'; }
$pages .= '' . esc_html( $prices ? I18n::t( 'Order summary' ) : I18n::t( 'Packing slip' ) ) . '
' . nl2br( esc_html( $store['address'] ) ) . '
'
. '- ' . esc_html( I18n::t( 'Order' ) ) . '
- #' . esc_html( $slip['number'] ) . '
- ' . esc_html( I18n::t( 'Date' ) ) . '
- ' . esc_html( $slip['date'] ) . '
'
. ( $prices ? '- ' . esc_html( I18n::t( 'Status' ) ) . '
- ' . esc_html( $slip['status'] ) . '
' : '' )
. ( $slip['method'] !== '' ? '- ' . esc_html( I18n::t( 'Delivery' ) ) . '
- ' . esc_html( $slip['method'] ) . '
' : '' ) . '
'
. ''
. ( $prices ? '
' . esc_html( I18n::t( 'Billing' ) ) . '
' . ( $slip['billing'] !== '' ? '
' . nl2br( esc_html( $slip['billing'] ) ) . '
' : '' ) . ( $slip['email'] !== '' ? '
' . esc_html( $slip['email'] ) . '
' : '' ) . '
' : '' )
. '
' . esc_html( $slip['pickup'] ? I18n::t( 'Store pickup' ) : I18n::t( 'Deliver to' ) ) . '
' . ( $slip['address'] !== '' ? '
' . nl2br( esc_html( $slip['address'] ) ) . '
' : '' ) . ( $slip['phone'] !== '' ? '
' . esc_html( $slip['phone'] ) . '
' : '' ) . '
'
. '
'
. ( $rows !== '' ? '| ' . esc_html( I18n::t( 'Product' ) ) . ' | ' . esc_html( I18n::t( 'SKU' ) ) . ' | ' . esc_html( I18n::t( 'Units' ) ) . ' | ' . ( $prices ? '' . esc_html( I18n::t( 'Price' ) ) . ' | ' : '' ) . '
' . $rows . '
' : '' . esc_html( I18n::t( 'Nothing left to send in this order.' ) ) . '
' )
. ( $totals !== '' ? '' : '' )
. ( $slip['note'] !== '' ? '' . esc_html( I18n::t( 'Customer note' ) ) . '
' . nl2br( esc_html( $slip['note'] ) ) . '
' : '' )
. implode( '', array_map( static fn( $extra ) => '' . esc_html( $extra['title'] ) . '
' . nl2br( esc_html( $extra['text'] ) ) . '
', $slip['notes'] ?? array() ) )
. ( $prices ? '' . esc_html( I18n::t( 'This order summary is not a tax invoice.' ) ) . '
' : '' )
. '';
}
return ''
. $pages
. ( $data['skipped'] ? '' . esc_html( I18n::format( $prices ? 'Left out (drafts): %s' : 'Left out (closed): %s', '#' . implode( ', #', $data['skipped'] ) ) ) . '
' : '' );
}
}