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 !== '' ? '' . ( $prices ? '' : '' ) . '' . $rows . '
' . esc_html( I18n::t( 'Product' ) ) . '' . esc_html( I18n::t( 'SKU' ) ) . '' . esc_html( I18n::t( 'Units' ) ) . '' . esc_html( I18n::t( 'Price' ) ) . '
' : '

' . esc_html( I18n::t( 'Nothing left to send in this order.' ) ) . '

' ) . ( $totals !== '' ? '' . $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'] ) ) ) . '

' : '' ); } }