class-wc-cli-report.php000066600000025276152140537160011073 0ustar00] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## EXAMPLES * * wp wc report list * * @subcommand list * @since 2.5.0 */ public function list_( $__, $assoc_args ) { $reports = array( 'sales', 'sales/top_sellers' ); $formatter = $this->get_formatter( array_merge( array( 'fields' => array_keys( $reports ) ), $assoc_args ) ); if ( 'ids' === $formatter->format ) { echo implode( ' ', $reports ); } else { $formatter->display_item( $reports ); } } /** * View sales report. * * ## OPTIONS * * [--field=] * : Instead of returning the whole report fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the report's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * [--period=] * : The supported periods are: week, month, last_month, and year. If invalid * period is supplied, week is used. If period is not specified, the current * day is used. * * [--date_min] * : Return sales for a specific start date. The date need to be in the YYYY-MM-AA format. * * [--date_max] * : Return sales for a specific end date. The dates need to be in the YYYY-MM-AA format. * * [--limit] * : Limit report result. Default: 12. * * ## AVAILABLE FIELDS * * These fields are available for get command: * * * total_sales * * average_sales * * total_orders * * total_items * * total_tax * * total_shipping * * total_discount * * totals_grouped_by * * totals * * total_customers * * ## EXAMPLES * * wp wc report sales * * wp wc report sales --period=last_month * * @since 2.5.0 */ public function sales( $__, $assoc_args ) { $reporter = $this->get_reporter( $assoc_args ); // new customers $users_query = new WP_User_Query( array( 'fields' => array( 'user_registered' ), 'role' => 'customer', ) ); $customers = $users_query->get_results(); foreach ( $customers as $key => $customer ) { if ( strtotime( $customer->user_registered ) < $reporter->start_date || strtotime( $customer->user_registered ) > $reporter->end_date ) { unset( $customers[ $key ] ); } } $total_customers = count( $customers ); $report_data = $reporter->get_report_data(); $period_totals = array(); // setup period totals by ensuring each period in the interval has data for ( $i = 0; $i <= $reporter->chart_interval; $i ++ ) { switch ( $reporter->chart_groupby ) { case 'day' : $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $reporter->start_date ) ); break; default : $time = date( 'Y-m', strtotime( "+{$i} MONTH", $reporter->start_date ) ); break; } // set the customer signups for each period $customer_count = 0; foreach ( $customers as $customer ) { if ( date( ( 'day' == $reporter->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { $customer_count++; } } $period_totals[ $time ] = array( 'sales' => wc_format_decimal( 0.00, 2 ), 'orders' => 0, 'items' => 0, 'tax' => wc_format_decimal( 0.00, 2 ), 'shipping' => wc_format_decimal( 0.00, 2 ), 'discount' => wc_format_decimal( 0.00, 2 ), 'customers' => $customer_count, ); } // add total sales, total order count, total tax and total shipping for each period foreach ( $report_data->orders as $order ) { $time = ( 'day' === $reporter->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); } foreach ( $report_data->order_counts as $order ) { $time = ( 'day' === $reporter->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['orders'] = (int) $order->count; } // add total order items for each period foreach ( $report_data->order_items as $order_item ) { $time = ( 'day' === $reporter->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; } // add total discount for each period foreach ( $report_data->coupons as $discount ) { $time = ( 'day' === $reporter->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); if ( ! isset( $period_totals[ $time ] ) ) { continue; } $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); } $sales_data = array( 'total_sales' => $report_data->total_sales, 'net_sales' => $report_data->net_sales, 'average_sales' => $report_data->average_sales, 'total_orders' => $report_data->total_orders, 'total_items' => $report_data->total_items, 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), 'total_shipping' => $report_data->total_shipping, 'total_refunds' => $report_data->total_refunds, 'total_discount' => $report_data->total_coupons, 'totals_grouped_by' => $reporter->chart_groupby, 'totals' => $period_totals, 'total_customers' => $total_customers, ); $sales_data = apply_filters( 'woocommerce_cli_sales_report', $sales_data ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $sales_data ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $sales_data ); } /** * View report of top sellers. * * ## OPTIONS * * [--=] * : Filter report based on report property. * * [--field=] * : Prints the value of a single field for each seller. * * [--fields=] * : Limit the output to specific report fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * [--period=] * : The supported periods are: week, month, last_month, and year. If invalid * period is supplied, week is used. If period is not specified, the current * day is used. * * [--date_min] * : Return sales for a specific start date. The date need to be in the YYYY-MM-AA format. * * [--date_max] * : Return sales for a specific end date. The dates need to be in the YYYY-MM-AA format. * * [--limit] * : Limit report result. Default: 12. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each row: * * * title * * product_id * * quantity * * ## EXAMPLES * * wp wc report top_sellers * * wp wc report top_sellers --period=last_month * * @since 2.5.0 */ public function top_sellers( $__, $assoc_args ) { $reporter = $this->get_reporter( $assoc_args ); $top_sellers = $reporter->get_order_report_data( array( 'data' => array( '_product_id' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => '', 'name' => 'product_id' ), '_qty' => array( 'type' => 'order_item_meta', 'order_item_type' => 'line_item', 'function' => 'SUM', 'name' => 'order_item_qty' ) ), 'order_by' => 'order_item_qty DESC', 'group_by' => 'product_id', 'limit' => isset( $assoc_args['limit'] ) ? absint( $assoc_args['limit'] ) : 12, 'query_type' => 'get_results', 'filter_range' => true, ) ); $top_sellers_data = array(); foreach ( $top_sellers as $top_seller ) { $product = wc_get_product( $top_seller->product_id ); if ( $product ) { $top_sellers_data[] = array( 'title' => $product->get_title(), 'product_id' => $top_seller->product_id, 'quantity' => $top_seller->order_item_qty, ); } } $top_sellers_data = apply_filters( 'woocommerce_cli_top_sellers_report', $top_sellers_data ); $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; echo implode( ' ', wp_list_pluck( $top_sellers_data, 'product_id' ) ); } else { $formatter->display_items( $top_sellers_data ); } } /** * Setup the report object and parse any date filtering * * @since 2.5.0 * @param array $assoc_args Arguments provided in when invoking the command * @return WC_Report_Sales_By_Date */ private function get_reporter( $assoc_args ) { include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); $report = new WC_Report_Sales_By_Date(); if ( empty( $assoc_args['period'] ) ) { // custom date range $assoc_args['period'] = 'custom'; if ( ! empty( $assoc_args['date_min'] ) || ! empty( $assoc_args['date_max'] ) ) { // overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges $_GET['start_date'] = $this->parse_datetime( $assoc_args['date_min'] ); $_GET['end_date'] = isset( $assoc_args['date_max'] ) ? $this->parse_datetime( $assoc_args['date_max'] ) : null; } else { // default custom range to today $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); } } else { // ensure period is valid if ( ! in_array( $assoc_args['period'], array( 'week', 'month', 'last_month', 'year' ) ) ) { $assoc_args['period'] = 'week'; } // TODO: change WC_Admin_Report class to use "week" instead, as it's more consistent with other periods // allow "week" for period instead of "7day" if ( 'week' === $assoc_args['period'] ) { $assoc_args['period'] = '7day'; } } $report->calculate_current_range( $assoc_args['period'] ); return $report; } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'title,product_id,quantity'; } } class-wc-cli-customer.php000066600000046420152140537160011413 0ustar00 * : The email address of the customer to create. * * [--=] * : Associative args for the new customer. * * [--porcelain] * : Outputs just the new customer id. * * ## AVAILABLE FIELDS * * These fields are optionally available for create command: * * * username * * password * * first_name * * last_name * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * ## EXAMPLES * * wp wc customer create new-customer@example.com --first_name=Akeda * * @since 2.5.0 */ public function create( $args, $assoc_args ) { global $wpdb; try { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $assoc_args['email'] = $args[0]; $data = apply_filters( 'woocommerce_cli_create_customer_data', $this->unflatten_array( $assoc_args ) ); // Sets the username. $data['username'] = ! empty( $data['username'] ) ? $data['username'] : ''; // Sets the password. $data['password'] = ! empty( $data['password'] ) ? $data['password'] : ''; // Attempts to create the new customer. $id = wc_create_new_customer( $data['email'], $data['username'], $data['password'] ); // Checks for an error in the customer creation. if ( is_wp_error( $id ) ) { throw new WC_CLI_Exception( $id->get_error_code(), $id->get_error_message() ); } // Added customer data. $this->update_customer_data( $id, $data ); do_action( 'woocommerce_cli_create_customer', $id, $data ); if ( $porcelain ) { WP_CLI::line( $id ); } else { WP_CLI::success( "Created customer $id." ); } } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Delete one or more customers. * * ## OPTIONS * * ... * : The customer ID, email, or username to delete. * * ## EXAMPLES * * wp wc customer delete 123 * * wp wc customer delete $(wp wc customer list --format=ids) * * @since 2.5.0 */ public function delete( $args, $assoc_args ) { $exit_code = 0; foreach ( $args as $arg ) { try { $customer = $this->get_user( $arg ); do_action( 'woocommerce_cli_delete_customer', $customer['id'] ); $r = wp_delete_user( $customer['id'] ); if ( $r ) { WP_CLI::success( "Deleted customer {$customer['id']}." ); } else { $exit_code += 1; WP_CLI::warning( "Failed deleting customer {$customer['id']}." ); } } catch ( WC_CLI_Exception $e ) { WP_CLI::warning( $e->getMessage() ); } } exit( $exit_code ? 1 : 0 ); } /** * View customer downloads. * * ## OPTIONS * * * : The customer ID, email or username. * * [--field=] * : Instead of returning the whole customer fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the customer's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * * download_id * * download_name * * access_expires * * ## EXAMPLES * * wp wc customer downloads 123 * * @since 2.5.0 */ public function downloads( $args, $assoc_args ) { try { $user = $this->get_user( $args[0] ); $downloads = array(); foreach ( wc_get_customer_available_downloads( $user['id'] ) as $key => $download ) { $downloads[ $key ] = $download; $downloads[ $key ]['access_expires'] = $this->format_datetime( $download['access_expires'] ); } $downloads = apply_filters( 'woocommerce_cli_customer_downloads', $downloads, $user, $assoc_args ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = $this->get_customer_download_fields(); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $downloads ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get a customer. * * ## OPTIONS * * * : Customer ID, email, or username. * * [--field=] * : Instead of returning the whole customer fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the customer's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * * id * * email * * first_name * * last_name * * created_at * * username * * last_order_id * * last_order_date * * orders_count * * total_spent * * avatar_url * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * Fields for filtering query result also available: * * * role Filter customers associated with certain role. * * q Filter customers with search query. * * created_at_min Filter customers whose registered after this date. * * created_at_max Filter customers whose registered before this date. * * limit The maximum returned number of results. * * offset Offset the returned results. * * order Accepted values: ASC and DESC. Default: DESC. * * orderby Sort retrieved customers by parameter. One or more options can be passed. * * ## EXAMPLES * * wp wc customer get 123 --field=email * * wp wc customer get customer-login --format=json * * @since 2.5.0 */ public function get( $args, $assoc_args ) { try { $user = $this->get_user( $args[0] ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $user ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $user ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List customers. * * ## OPTIONS * * [--=] * : Filter customer based on customer property. * * [--field=] * : Prints the value of a single field for each customer. * * [--fields=] * : Limit the output to specific customer fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each customer: * * * id * * email * * first_name * * last_name * * created_at * * These fields are optionally available: * * * username * * last_order_id * * last_order_date * * orders_count * * total_spent * * avatar_url * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * Fields for filtering query result also available: * * * role Filter customers associated with certain role. * * q Filter customers with search query. * * created_at_min Filter customers whose registered after this date. * * created_at_max Filter customers whose registered before this date. * * limit The maximum returned number of results. * * offset Offset the returned results. * * order Accepted values: ASC and DESC. Default: DESC. * * orderby Sort retrieved customers by parameter. One or more options can be passed. * * ## EXAMPLES * * wp wc customer list * * wp wc customer list --field=id * * wp wc customer list --fields=id,email,first_name --format=json * * @subcommand list * @since 2.5.0 */ public function list_( $__, $assoc_args ) { $query_args = $this->merge_wp_user_query_args( $this->get_list_query_args(), $assoc_args ); $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_User_Query( $query_args ); echo implode( ' ', $query->results ); } else { $query = new WP_User_Query( $query_args ); $items = $this->format_users_to_items( $query->results ); $formatter->display_items( $items ); } } /** * View customer orders. * * ## OPTIONS * * * : The customer ID, email or username. * * [--field=] * : Instead of returning the whole customer fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the customer's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * For more fields, see: wp wc order list --help * * ## EXAMPLES * * wp wc customer orders 123 * * @since 2.5.0 */ public function orders( $args, $assoc_args ) { try { WP_CLI::run_command( array( 'wc', 'order', 'list' ), array( 'customer_id' => $args[0] ) ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Update one or more customers. * * ## OPTIONS * * * : Customer ID, email, or username. * * [--=] * : One or more fields to update. * * ## AVAILABLE FIELDS * * These fields are available for update command: * * * email * * password * * first_name * * last_name * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * ## EXAMPLES * * wp wc customer update customer-login --first_name=akeda --last_name=bagus * * wp wc customer update customer@example.com --password=new-password * * @since 2.5.0 */ public function update( $args, $assoc_args ) { try { $user = $this->get_user( $args[0] ); $data = $this->unflatten_array( $assoc_args ); $data = apply_filters( 'woocommerce_cli_update_customer_data', $data ); // Customer email. if ( isset( $data['email'] ) ) { wp_update_user( array( 'ID' => $user['id'], 'user_email' => sanitize_email( $data['email'] ) ) ); } // Customer password. if ( isset( $data['password'] ) ) { wp_update_user( array( 'ID' => $user['id'], 'user_pass' => wc_clean( $data['password'] ) ) ); } // Update customer data. $this->update_customer_data( $user['id'], $data ); do_action( 'woocommerce_cli_update_customer', $user['id'], $data ); WP_CLI::success( "Updated customer {$user['id']}." ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get query args for list subcommand. * * @since 2.5.0 * @return array */ protected function get_list_query_args() { return array( 'role' => 'customer', 'orderby' => 'registered', ); } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,email,first_name,last_name,created_at'; } /** * Format users from WP_User_Query result to items in which each item contain * common properties of item. * * @since 2.5.0 * @param array $users Array of user * @return array Items */ protected function format_users_to_items( $users ) { $items = array(); foreach ( $users as $user ) { try { $items[] = $this->get_user( $user->ID ); } catch ( WC_CLI_Exception $e ) { WP_CLI::warning( $e->getMessage() ); } } return $items; } /** * Get user from given user ID, email, or login * * @throws WC_CLI_Exception * * @since 2.5.0 * @param mixed $id_email_or_login * @return array|WP_Error */ protected function get_user( $id_email_or_login ) { global $wpdb; if ( is_numeric( $id_email_or_login ) ) { $user = get_user_by( 'id', $id_email_or_login ); } else if ( is_email( $id_email_or_login ) ) { $user = get_user_by( 'email', $id_email_or_login ); } else { $user = get_user_by( 'login', $id_email_or_login ); } if ( ! $user ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_customer', sprintf( __( 'Invalid customer "%s"', 'woocommerce' ), $id_email_or_login ) ); } // Get info about user's last order $last_order = $wpdb->get_row( "SELECT id, post_date_gmt FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = {$user->ID} AND posts.post_type = 'shop_order' AND posts.post_status IN ( '" . implode( "','", array_keys( wc_get_order_statuses() ) ) . "' ) ORDER BY posts.ID DESC " ); $customer = array( 'id' => $user->ID, 'created_at' => $this->format_datetime( $user->user_registered ), 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'username' => $user->user_login, 'role' => $user->roles[0], 'last_order_id' => is_object( $last_order ) ? $last_order->id : null, 'last_order_date' => is_object( $last_order ) ? $this->format_datetime( $last_order->post_date_gmt ) : null, 'orders_count' => wc_get_customer_order_count( $user->ID ), 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $user->ID ), 2 ), 'avatar_url' => $this->get_avatar_url( $user->customer_email ), 'billing_address' => array( 'first_name' => $user->billing_first_name, 'last_name' => $user->billing_last_name, 'company' => $user->billing_company, 'address_1' => $user->billing_address_1, 'address_2' => $user->billing_address_2, 'city' => $user->billing_city, 'state' => $user->billing_state, 'postcode' => $user->billing_postcode, 'country' => $user->billing_country, 'email' => $user->billing_email, 'phone' => $user->billing_phone, ), 'shipping_address' => array( 'first_name' => $user->shipping_first_name, 'last_name' => $user->shipping_last_name, 'company' => $user->shipping_company, 'address_1' => $user->shipping_address_1, 'address_2' => $user->shipping_address_2, 'city' => $user->shipping_city, 'state' => $user->shipping_state, 'postcode' => $user->shipping_postcode, 'country' => $user->shipping_country, ), ); // Allow dot notation for nested array so that user can specifies field // like 'billing_address.first_name'. return $this->flatten_array( $customer ); } /** * Wrapper for @see get_avatar() which doesn't simply return * the URL so we need to pluck it from the HTML img tag * * Kudos to https://github.com/WP-API/WP-API for offering a better solution * * @since 2.5.0 * @param string $email the customer's email * @return string the URL to the customer's avatar */ protected function get_avatar_url( $email ) { $avatar_html = get_avatar( $email ); // Get the URL of the avatar from the provided HTML preg_match( '/src=["|\'](.+)[\&|"|\']/U', $avatar_html, $matches ); if ( isset( $matches[1] ) && ! empty( $matches[1] ) ) { return esc_url_raw( $matches[1] ); } return null; } /** * Add/Update customer data. * * @since 2.5.0 * @param int $id The customer ID * @param array $data */ protected function update_customer_data( $id, $data ) { // Customer first name. if ( isset( $data['first_name'] ) ) { update_user_meta( $id, 'first_name', wc_clean( $data['first_name'] ) ); } // Customer last name. if ( isset( $data['last_name'] ) ) { update_user_meta( $id, 'last_name', wc_clean( $data['last_name'] ) ); } // Customer billing address. if ( isset( $data['billing_address'] ) ) { foreach ( $this->get_customer_billing_address_fields() as $address ) { if ( isset( $data['billing_address'][ $address ] ) ) { update_user_meta( $id, 'billing_' . $address, wc_clean( $data['billing_address'][ $address ] ) ); } } } // Customer shipping address. if ( isset( $data['shipping_address'] ) ) { foreach ( $this->get_customer_shipping_address_fields() as $address ) { if ( isset( $data['shipping_address'][ $address ] ) ) { update_user_meta( $id, 'shipping_' . $address, wc_clean( $data['shipping_address'][ $address ] ) ); } } } do_action( 'woocommerce_cli_update_customer_data', $id, $data ); } /** * Get customer billing address fields. * * @since 2.5.0 * @return array */ protected function get_customer_billing_address_fields() { return apply_filters( 'woocommerce_cli_customer_billing_address_fields', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', 'email', 'phone', ) ); } /** * Get customer shipping address fields. * * @since 2.5.0 * @return array */ protected function get_customer_shipping_address_fields() { return apply_filters( 'woocommerce_cli_customer_shipping_address_fields', array( 'first_name', 'last_name', 'company', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ) ); } /** * Get customer download fields. * * @since 2.5.0 * @return array */ protected function get_customer_download_fields() { return apply_filters( 'woocommerce_cli_customer_download_fields', array( 'download_id', 'download_name', 'access_expires', ) ); } } class-wc-cli-exception.php000066600000001723152140537160011545 0ustar00error_code = $error_code; parent::__construct( $error_message ); } /** * Returns the error code * * @since 2.5.0 * @return string */ public function getErrorCode() { return $this->error_code; } } class-wc-cli-tool.php000066600000001120152140537160010513 0ustar00 * : Product category ID. * * [--field=] * : Instead of returning the whole product category fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the product category's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * * id * * name * * slug * * parent * * description * * display * * image * * count * * ## EXAMPLES * * wp wc product category get 123 * * @since 2.5.0 */ public function get( $args, $assoc_args ) { try { $product_category = $this->get_product_category( $args[0] ); $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $product_category ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List of product categories. * * ## OPTIONS * * [--=] * : Filter products based on product property. * * [--field=] * : Prints the value of a single field for each product. * * [--fields=] * : Limit the output to specific product fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * * id * * name * * slug * * parent * * description * * display * * image * * count * * ## EXAMPLES * * wp wc product category list * * wp wc product category list --fields=id,name --format=json * * @subcommand list * @since 2.5.0 */ public function list_( $__, $assoc_args ) { try { $product_categories = array(); $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); foreach ( $terms as $term_id ) { $product_categories[] = $this->get_product_category( $term_id ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $product_categories ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get product category properties from given term ID. * * @since 2.5.0 * @param int $term_id Category term ID * @return array * @throws WC_CLI_Exception */ protected function get_product_category( $term_id ) { $term_id = absint( $term_id ); $term = get_term( $term_id, 'product_cat' ); if ( is_wp_error( $term ) || is_null( $term ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_category_id', sprintf( __( 'Invalid product category ID "%s"', 'woocommerce' ), $term_id ) ); } $term_id = intval( $term->term_id ); // Get category display type. $display_type = get_woocommerce_term_meta( $term_id, 'display_type' ); // Get category image. $image = ''; if ( $image_id = get_woocommerce_term_meta( $term_id, 'thumbnail_id' ) ) { $image = wp_get_attachment_url( $image_id ); } return array( 'id' => $term_id, 'name' => $term->name, 'slug' => $term->slug, 'parent' => $term->parent, 'description' => $term->description, 'display' => $display_type ? $display_type : 'default', 'image' => $image ? esc_url( $image ) : '', 'count' => intval( $term->count ) ); } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,name,slug,parent,description,display,image,count'; } } class-wc-cli-product.php000066600000205407152140537160011234 0ustar00=] * : Associative args for the new product. * * [--porcelain] * : Outputs just the new product id. * * ## AVAILABLE FIELDS * * Required fields: * * * title * * These fields are optionally available for create command: * * * type * * status * * downloadable * * virtual * * sku * * regular_price * * sale_price * * sale_price_dates_from * * sale_price_dates_to * * tax_status * * tax_class * * managing_stock * * stock_quantity * * in_stock * * backorders * * sold_individually * * featured * * shipping_class * * description * * enable_html_description * * short_description * * enable_html_short_description * * reviews_allowed * * upsell_ids * * cross_sell_ids * * parent_id * * categories * * tags * * Dimensions fields: * * * dimensions.length * * dimensions.width * * dimensions.height * * dimensions.unit * * Images is an array in which element can be set by specifying its index: * * * images * * images.size * * images.0.id * * images.0.created_at * * images.0.updated_at * * images.0.src * * images.0.title * * images.0.alt * * images.0.position * * Attributes is an array in which element can be set by specifying its index: * * * attributes * * attributes.size * * attributes.0.name * * attributes.0.slug * * attributes.0.position * * attributes.0.visible * * attributes.0.variation * * attributes.0.options * * Downloads is an array in which element can be accessed by specifying its index: * * * downloads * * downloads.size * * downloads.0.id * * downloads.0.name * * downloads.0.file * * Variations is an array in which element can be accessed by specifying its index: * * * variations * * variations.size * * variations.0.id * * variations.0.created_at * * variations.0.updated_at * * variations.0.downloadable * * variations.0.virtual * * variations.0.permalink * * variations.0.sku * * variations.0.price * * variations.0.regular_price * * variations.0.sale_price * * variations.0.sale_price_dates_from * * variations.0.sale_price_dates_to * * variations.0.taxable * * variations.0.tax_status * * variations.0.tax_class * * variations.0.managing_stock * * variations.0.stock_quantity * * variations.0.in_stock * * variations.0.backordered * * variations.0.purchaseable * * variations.0.visible * * variations.0.on_sale * * variations.0.weight * * variations.0.dimensions -- See dimensions fields * * variations.0.shipping_class * * variations.0.shipping_class_id * * variations.0.images -- See images fields * * variations.0.attributes -- See attributes fields * * variations.0.downloads -- See downloads fields * * variations.0.download_limit * * variations.0.download_expiry * * ## EXAMPLES * * wp wc product create --title="Product Name" * * wp wc product create --title="Product Name" -- * * @since 2.5.0 */ public function create( $args, $assoc_args ) { $id = 0; try { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $data = apply_filters( 'woocommerce_cli_create_product_data', $this->unflatten_array( $assoc_args ) ); // Check if product title is specified if ( ! isset( $data['title'] ) ) { throw new WC_CLI_Exception( 'woocommerce_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ) ); } // Check product type if ( ! isset( $data['type'] ) ) { $data['type'] = 'simple'; } // Set visible visibility when not sent if ( ! isset( $data['catalog_visibility'] ) ) { $data['catalog_visibility'] = 'visible'; } // Validate the product type if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ) ); } // Enable description html tags. $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; if ( $post_content && isset( $data['enable_html_description'] ) && $this->is_true( $data['enable_html_description'] ) ) { $post_content = $data['description']; } // Enable short description html tags. $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && $this->is_true( $data['enable_html_short_description'] ) ) { $post_excerpt = $data['short_description']; } $new_product = array( 'post_title' => wc_clean( $data['title'] ), 'post_status' => ( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ), 'post_type' => 'product', 'post_excerpt' => ( isset( $data['short_description'] ) ? $post_excerpt : '' ), 'post_content' => ( isset( $data['description'] ) ? $post_content : '' ), 'post_author' => get_current_user_id(), ); // Attempts to create the new product $id = wp_insert_post( $new_product, true ); // Checks for an error in the product creation if ( is_wp_error( $id ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_create_product', $id->get_error_message() ); } // Check for featured/gallery images, upload it and set it if ( isset( $data['images'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields $this->save_product_meta( $id, $data ); // Save variations if ( $this->is_variable( $data ) ) { $this->save_variations( $id, $data ); } do_action( 'woocommerce_cli_create_product', $id, $data ); // Clear cache/transients wc_delete_product_transients( $id ); if ( $porcelain ) { WP_CLI::line( $id ); } else { WP_CLI::success( "Created product $id." ); } } catch ( WC_CLI_Exception $e ) { // Remove the product when fails $this->clear_product( $id ); WP_CLI::error( $e->getMessage() ); } } /** * Delete products. * * ## OPTIONS * * ... * : The product ID to delete. * * ## EXAMPLES * * wp wc product delete 123 * * wp wc product delete $(wp wc product list --format=ids) * * @since 2.5.0 */ public function delete( $args, $assoc_args ) { $exit_code = 0; foreach ( $args as $id ) { do_action( 'woocommerce_cli_delete_product', $id ); $r = wp_delete_post( $id, true ); if ( $r ) { WP_CLI::success( "Deleted product $id." ); } else { $exit_code += 1; WP_CLI::warning( "Failed deleting product $id." ); } } exit( $exit_code ? 1 : 0 ); } /** * Get a product. * * ## OPTIONS * * * : Product ID. * * [--field=] * : Instead of returning the whole product fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the product's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * For more fields, see: wp wc product list --help * * ## EXAMPLES * * wp wc product get 123 --fields=id,title,sku * * @since 2.5.0 */ public function get( $args, $assoc_args ) { try { $product = wc_get_product( $args[0] ); if ( ! $product ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product', sprintf( __( 'Invalid product "%s"', 'woocommerce' ), $args[0] ) ); } $product_data = $this->get_product_data( $product ); $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $product_data ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List products. * * ## OPTIONS * * [--=] * : Filter products based on product property. * * [--field=] * : Prints the value of a single field for each product. * * [--fields=] * : Limit the output to specific product fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each product: * * * id * * title * * sku * * in_stock * * price * * sale_price * * categories * * tags * * type * * created_at * * These fields are optionally available: * * * updated_at * * status * * downloadable * * virtual * * permalink * * regular_price * * sale_price_dates_from * * sale_price_dates_to * * price_html * * taxable * * tax_status * * tax_class * * managing_stock * * stock_quantity * * backorders_allowed * * backordered * * backorders * * sold_individually * * purchaseable * * featured * * visible * * catalog_visibility * * on_sale * * weight * * shipping_required * * shipping_taxable * * shipping_class * * shipping_class_id * * description * * enable_html_description * * short_description * * enable_html_short_description * * reviews_allowed * * average_rating * * rating_count * * related_ids * * upsell_ids * * cross_sell_ids * * parent_id * * featured_src * * download_limit * * download_expiry * * download_type * * purchase_note * * total_sales * * parent * * product_url * * button_text * * There are some properties that are nested array. In such case, if array.size * is zero then listing the fields with `array.0.some_field` will results * in error that field `array.0.some_field` does not exists. * * Dimensions fields: * * * dimensions.length * * dimensions.width * * dimensions.height * * dimensions.unit * * Images is an array in which element can be accessed by specifying its index: * * * images * * images.size * * images.0.id * * images.0.created_at * * images.0.updated_at * * images.0.src * * images.0.title * * images.0.alt * * images.0.position * * Attributes is an array in which element can be accessed by specifying its index: * * * attributes * * attributes.size * * attributes.0.name * * attributes.0.slug * * attributes.0.position * * attributes.0.visible * * attributes.0.variation * * attributes.0.options * * Downloads is an array in which element can be accessed by specifying its index: * * * downloads * * downloads.size * * downloads.0.id * * downloads.0.name * * downloads.0.file * * Variations is an array in which element can be accessed by specifying its index: * * * variations * * variations.size * * variations.0.id * * variations.0.created_at * * variations.0.updated_at * * variations.0.downloadable * * variations.0.virtual * * variations.0.permalink * * variations.0.sku * * variations.0.price * * variations.0.regular_price * * variations.0.sale_price * * variations.0.sale_price_dates_from * * variations.0.sale_price_dates_to * * variations.0.taxable * * variations.0.tax_status * * variations.0.tax_class * * variations.0.managing_stock * * variations.0.stock_quantity * * variations.0.in_stock * * variations.0.backordered * * variations.0.purchaseable * * variations.0.visible * * variations.0.on_sale * * variations.0.weight * * variations.0.dimensions -- See dimensions fields * * variations.0.shipping_class * * variations.0.shipping_class_id * * variations.0.images -- See images fields * * variations.0.attributes -- See attributes fields * * variations.0.downloads -- See downloads fields * * variations.0.download_limit * * variations.0.download_expiry * * Fields for filtering query result also available: * * * q Filter products with search query. * * created_at_min Filter products whose created after this date. * * created_at_max Filter products whose created before this date. * * updated_at_min Filter products whose updated after this date. * * updated_at_max Filter products whose updated before this date. * * limit The maximum returned number of results. * * offset Offset the returned results. * * order Accepted values: ASC and DESC. Default: DESC. * * orderby Sort retrieved products by parameter. One or more options can be passed. * * ## EXAMPLES * * wp wc product list * * wp wc product list --field=id * * wp wc product list --fields=id,title,type --format=json * * @subcommand list * @since 2.5.0 */ public function list_( $args, $assoc_args ) { $query_args = $this->merge_wp_query_args( $this->get_list_query_args( $assoc_args ), $assoc_args ); $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_Query( $query_args ); echo implode( ' ', $query->posts ); } else { $query = new WP_Query( $query_args ); $items = $this->format_posts_to_items( $query->posts ); $formatter->display_items( $items ); } } /** * List of product reviews. * * ## OPTIONS * * * : Product ID. * * [--field=] * : Instead of returning the whole review fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the review's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * * id * * rating * * reviewer_name * * reviewer_email * * verified * * created_at * * ## EXAMPLES * * wp wc product reviews 123 * * wp wc product reviews 123 --fields=id,rating,reviewer_email * * @since 2.5.0 */ public function reviews( $args, $assoc_args ) { try { $id = $args[0]; $product = wc_get_product( $id ); if ( ! $product ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product', sprintf( __( 'Invalid product "%s"', 'woocommerce' ), $id ) ); } $comments = get_approved_comments( $id ); $reviews = array(); foreach ( $comments as $comment ) { $reviews[] = array( 'id' => intval( $comment->comment_ID ), 'created_at' => $this->format_datetime( $comment->comment_date_gmt ), 'review' => $comment->comment_content, 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), 'reviewer_name' => $comment->comment_author, 'reviewer_email' => $comment->comment_author_email, 'verified' => (bool) get_comment_meta( $comment->comment_ID, 'verified', true ), ); } if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = $this->get_review_fields(); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $reviews ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get product types. * * ## EXAMPLES * * wp wc product types * * @since 2.5.0 */ public function types( $__, $___ ) { $product_types = wc_get_product_types(); foreach ( $product_types as $type => $label ) { WP_CLI::line( sprintf( '%s: %s', $label, $type ) ); } } /** * Update one or more products. * * ## OPTIONS * * * : Product ID * * [--=] * : One or more fields to update. * * ## AVAILABLE_FIELDS * * For more fields, see: wp wc product create --help * * ## EXAMPLES * * wp wc product update 123 --title="New Product Title" --description="New description" * * @since 2.5.0 */ public function update( $args, $assoc_args ) { try { $id = $args[0]; $data = apply_filters( 'woocommerce_cli_update_product_data', $this->unflatten_array( $assoc_args ) ); // Product title. if ( isset( $data['title'] ) ) { wp_update_post( array( 'ID' => $id, 'post_title' => wc_clean( $data['title'] ) ) ); } // Product name (slug). if ( isset( $data['name'] ) ) { wp_update_post( array( 'ID' => $id, 'post_name' => sanitize_title( $data['name'] ) ) ); } // Product status. if ( isset( $data['status'] ) ) { wp_update_post( array( 'ID' => $id, 'post_status' => wc_clean( $data['status'] ) ) ); } // Product short description. if ( isset( $data['short_description'] ) ) { // Enable short description html tags. $post_excerpt = ( isset( $data['enable_html_short_description'] ) && $this->is_true( $data['enable_html_short_description'] ) ) ? $data['short_description'] : wc_clean( $data['short_description'] ); wp_update_post( array( 'ID' => $id, 'post_excerpt' => $post_excerpt ) ); } // Product description. if ( isset( $data['description'] ) ) { // Enable description html tags. $post_content = ( isset( $data['enable_html_description'] ) && $this->is_true( $data['enable_html_description'] ) ) ? $data['description'] : wc_clean( $data['description'] ); wp_update_post( array( 'ID' => $id, 'post_content' => $post_content ) ); } // Validate the product type if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ) ); } // Check for featured/gallery images, upload it and set it if ( isset( $data['images'] ) ) { $this->save_product_images( $id, $data['images'] ); } // Save product meta fields $this->save_product_meta( $id, $data ); // Save variations if ( $this->is_variable( $data ) ) { $this->save_variations( $id, $data ); } do_action( 'woocommerce_cli_update_product', $id, $data ); // Clear cache/transients wc_delete_product_transients( $id ); WP_CLI::success( "Updated product $id." ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get query args for list subcommand. * * @since 2.5.0 * @param array $args Args from command line * @return array */ protected function get_list_query_args( $args ) { $query_args = array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array(), ); if ( ! empty( $args['type'] ) ) { $types = explode( ',', $args['type'] ); $query_args['tax_query'] = array( array( 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $types, ), ); } // Filter products by category if ( ! empty( $args['category'] ) ) { $query_args['product_cat'] = $args['category']; } // Filter by specific sku if ( ! empty( $args['sku'] ) ) { if ( ! is_array( $query_args['meta_query'] ) ) { $query_args['meta_query'] = array(); } $query_args['meta_query'][] = array( 'key' => '_sku', 'value' => $args['sku'], 'compare' => '=' ); } return $query_args; } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,title,sku,in_stock,price,sale_price,categories,tags,type,created_at'; } /** * Get review fields for formatting. * * @since 2.5.0 * @return string */ protected function get_review_fields() { return 'id,rating,reviewer_name,reviewer_email,verified,created_at'; } /** * Format posts from WP_Query result to items in which each item contain * common properties of item, for instance `post_title` will be `title`. * * @since 2.5.0 * @param array $posts Array of post * @return array Items */ protected function format_posts_to_items( $posts ) { $items = array(); foreach ( $posts as $post ) { $product = wc_get_product( $post->ID ); if ( ! $product ) { continue; } $items[] = $this->get_product_data( $product ); } return $items; } /** * Get standard product data that applies to every product type. * * @since 2.5.0 * @param WC_Product $product * @return array */ private function get_product_data( $product ) { // Add data that applies to every product type. $product_data = array( 'title' => $product->get_title(), 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'created_at' => $this->format_datetime( $product->get_post_data()->post_date_gmt ), 'updated_at' => $this->format_datetime( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_status, 'downloadable' => $product->is_downloadable(), 'virtual' => $product->is_virtual(), 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, 'price_html' => $product->get_price_html(), 'taxable' => $product->is_taxable(), 'tax_status' => $product->get_tax_status(), 'tax_class' => $product->get_tax_class(), 'managing_stock' => $product->managing_stock(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock() ? 'yes' : 'no', 'backorders_allowed' => $product->backorders_allowed(), 'backordered' => $product->is_on_backorder(), 'sold_individually' => $product->is_sold_individually(), 'purchaseable' => $product->is_purchasable(), 'featured' => $product->is_featured(), 'visible' => $product->is_visible(), 'catalog_visibility' => $product->visibility, 'on_sale' => $product->is_on_sale(), 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', 'weight' => $product->get_weight() ? $product->get_weight() : null, 'dimensions' => array( 'length' => $product->length, 'width' => $product->width, 'height' => $product->height, 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_required' => $product->needs_shipping(), 'shipping_taxable' => $product->is_shipping_taxable(), 'shipping_class' => $product->get_shipping_class(), 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), 'rating_count' => (int) $product->get_rating_count(), 'related_ids' => implode( ', ', $product->get_related() ), 'upsell_ids' => implode( ', ', $product->get_upsells() ), 'cross_sell_ids' => implode( ', ', $product->get_cross_sells() ), 'parent_id' => $product->post->post_parent, 'categories' => implode( ', ', wp_get_post_terms( $product->id, 'product_cat', array( 'fields' => 'names' ) ) ), 'tags' => implode( ', ', wp_get_post_terms( $product->id, 'product_tag', array( 'fields' => 'names' ) ) ), 'images' => $this->get_images( $product ), 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->is_type( 'variation' ) ? $product->variation_id : $product->id ) ), 'attributes' => $this->get_attributes( $product ), 'downloads' => $this->get_downloads( $product ), 'download_limit' => (int) $product->download_limit, 'download_expiry' => (int) $product->download_expiry, 'download_type' => $product->download_type, 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->purchase_note ) ) ), 'total_sales' => metadata_exists( 'post', $product->id, 'total_sales' ) ? (int) get_post_meta( $product->id, 'total_sales', true ) : 0, 'variations' => array(), 'parent' => array(), ); // add variations to variable products if ( $product->is_type( 'variable' ) && $product->has_child() ) { $product_data['variations'] = $this->get_variation_data( $product ); } // add the parent product data to an individual variation if ( $product->is_type( 'variation' ) ) { $product_data['parent'] = $this->get_product_data( $product->parent ); } return $this->flatten_array( $product_data ); } /** * Get the images for a product or product variation * * @since 2.5.0 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_images( $product ) { $images = $attachment_ids = array(); if ( $product->is_type( 'variation' ) ) { if ( has_post_thumbnail( $product->get_variation_id() ) ) { // Add variation image if set $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); } elseif ( has_post_thumbnail( $product->id ) ) { // Otherwise use the parent product featured image if set $attachment_ids[] = get_post_thumbnail_id( $product->id ); } } else { // Add featured image if ( has_post_thumbnail( $product->id ) ) { $attachment_ids[] = get_post_thumbnail_id( $product->id ); } // Add gallery images $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_ids() ); } // Build image data foreach ( $attachment_ids as $position => $attachment_id ) { $attachment_post = get_post( $attachment_id ); if ( is_null( $attachment_post ) ) { continue; } $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); if ( ! is_array( $attachment ) ) { continue; } $images[] = array( 'id' => (int) $attachment_id, 'created_at' => $this->format_datetime( $attachment_post->post_date_gmt ), 'updated_at' => $this->format_datetime( $attachment_post->post_modified_gmt ), 'src' => current( $attachment ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 'position' => (int) $position, ); } // Set a placeholder image if the product has no images set if ( empty( $images ) ) { $images[] = array( 'id' => 0, 'created_at' => $this->format_datetime( time() ), // Default to now 'updated_at' => $this->format_datetime( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), 'position' => 0, ); } return $images; } /** * Get the attributes for a product or product variation * * @since 2.5.0 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_attributes( $product ) { $attributes = array(); if ( $product->is_type( 'variation' ) ) { // variation attributes foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` $attributes[] = array( 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ) ), 'slug' => str_replace( 'attribute_', '', str_replace( 'pa_', '', $attribute_name ) ), 'option' => $attribute, ); } } else { foreach ( $product->get_attributes() as $attribute ) { // taxonomy-based attributes are comma-separated, others are pipe (|) separated if ( $attribute['is_taxonomy'] ) { $options = explode( ',', $product->get_attribute( $attribute['name'] ) ); } else { $options = explode( '|', $product->get_attribute( $attribute['name'] ) ); } $attributes[] = array( 'name' => wc_attribute_label( $attribute['name'] ), 'slug' => str_replace( 'pa_', '', $attribute['name'] ), 'position' => (int) $attribute['position'], 'visible' => (bool) $attribute['is_visible'], 'variation' => (bool) $attribute['is_variation'], 'options' => array_map( 'trim', $options ), ); } } return $attributes; } /** * Get the downloads for a product or product variation * * @since 2.5.0 * @param WC_Product|WC_Product_Variation $product * @return array */ private function get_downloads( $product ) { $downloads = array(); if ( $product->is_downloadable() ) { foreach ( $product->get_files() as $file_id => $file ) { $downloads[] = array( 'id' => $file_id, // do not cast as int as this is a hash 'name' => $file['name'], 'file' => $file['file'], ); } } return $downloads; } /** * Get an individual variation's data * * @since 2.5.0 * @param WC_Product $product * @return array */ private function get_variation_data( $product ) { $variations = array(); foreach ( $product->get_children() as $child_id ) { $variation = $product->get_child( $child_id ); if ( ! $variation->exists() ) { continue; } $variations[] = array( 'id' => $variation->get_variation_id(), 'created_at' => $this->format_datetime( $variation->get_post_data()->post_date_gmt ), 'updated_at' => $this->format_datetime( $variation->get_post_data()->post_modified_gmt ), 'downloadable' => $variation->is_downloadable(), 'virtual' => $variation->is_virtual(), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, 'taxable' => $variation->is_taxable(), 'tax_status' => $variation->get_tax_status(), 'tax_class' => $variation->get_tax_class(), 'managing_stock' => $variation->managing_stock(), 'stock_quantity' => $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'backordered' => $variation->is_on_backorder(), 'purchaseable' => $variation->is_purchasable(), 'visible' => $variation->variation_is_visible(), 'on_sale' => $variation->is_on_sale(), 'weight' => $variation->get_weight() ? $variation->get_weight() : null, 'dimensions' => array( 'length' => $variation->length, 'width' => $variation->width, 'height' => $variation->height, 'unit' => get_option( 'woocommerce_dimension_unit' ), ), 'shipping_class' => $variation->get_shipping_class(), 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, 'image' => $this->get_images( $variation ), 'attributes' => $this->get_attributes( $variation ), 'downloads' => $this->get_downloads( $variation ), 'download_limit' => (int) $product->download_limit, 'download_expiry' => (int) $product->download_expiry, ); } return $variations; } /** * Save product meta * * @since 2.5.0 * @param int $product_id * @param array $data * @return bool * @throws WC_CLI_Exception */ private function save_product_meta( $product_id, $data ) { global $wpdb; // Product Type $product_type = null; if ( isset( $data['type'] ) ) { $product_type = wc_clean( $data['type'] ); wp_set_object_terms( $product_id, $product_type, 'product_type' ); } else { $_product_type = get_the_terms( $product_id, 'product_type' ); if ( is_array( $_product_type ) ) { $_product_type = current( $_product_type ); $product_type = $_product_type->slug; } } // Virtual if ( isset( $data['virtual'] ) ) { update_post_meta( $product_id, '_virtual', ( $this->is_true( $data['virtual'] ) ) ? 'yes' : 'no' ); } // Tax status if ( isset( $data['tax_status'] ) ) { update_post_meta( $product_id, '_tax_status', wc_clean( $data['tax_status'] ) ); } // Tax Class if ( isset( $data['tax_class'] ) ) { update_post_meta( $product_id, '_tax_class', wc_clean( $data['tax_class'] ) ); } // Catalog Visibility if ( isset( $data['catalog_visibility'] ) ) { update_post_meta( $product_id, '_visibility', wc_clean( $data['catalog_visibility'] ) ); } // Purchase Note if ( isset( $data['purchase_note'] ) ) { update_post_meta( $product_id, '_purchase_note', wc_clean( $data['purchase_note'] ) ); } // Featured Product if ( isset( $data['featured'] ) ) { update_post_meta( $product_id, '_featured', ( $this->is_true( $data['featured'] ) ) ? 'yes' : 'no' ); } // Shipping data $this->save_product_shipping_data( $product_id, $data ); // SKU if ( isset( $data['sku'] ) ) { $sku = get_post_meta( $product_id, '_sku', true ); $new_sku = wc_clean( $data['sku'] ); if ( '' == $new_sku ) { update_post_meta( $product_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $product_id, $new_sku ); if ( ! $unique_sku ) { throw new WC_CLI_Exception( 'woocommerce_cli_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ) ); } else { update_post_meta( $product_id, '_sku', $new_sku ); } } else { update_post_meta( $product_id, '_sku', '' ); } } } // Attributes if ( isset( $data['attributes'] ) ) { $attributes = array(); foreach ( $data['attributes'] as $attribute ) { $is_taxonomy = 0; $taxonomy = 0; if ( ! isset( $attribute['name'] ) ) { continue; } $attribute_slug = sanitize_title( $attribute['name'] ); if ( isset( $attribute['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); $attribute_slug = sanitize_title( $attribute['slug'] ); } if ( $taxonomy ) { $is_taxonomy = 1; } if ( $is_taxonomy ) { if ( isset( $attribute['options'] ) ) { $options = $attribute['options']; if ( ! is_array( $attribute['options'] ) ) { // Text based attributes - Posted values are term names $options = explode( WC_DELIMITER, $options ); } $values = array_map( 'wc_sanitize_term_text_based', $options ); $values = array_filter( $values, 'strlen' ); } else { $values = array(); } // Update post terms if ( taxonomy_exists( $taxonomy ) ) { wp_set_object_terms( $product_id, $values, $taxonomy ); } if ( ! empty( $values ) ) { // Add attribute to array, but don't set values $attributes[ $taxonomy ] = array( 'name' => $taxonomy, 'value' => '', 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } elseif ( isset( $attribute['options'] ) ) { // Array based if ( is_array( $attribute['options'] ) ) { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); // Text based, separate by pipe } else { $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); } // Custom attribute - Add attribute to array and set the values $attributes[ $attribute_slug ] = array( 'name' => wc_clean( $attribute['name'] ), 'value' => $values, 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, 'is_taxonomy' => $is_taxonomy ); } } uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); update_post_meta( $product_id, '_product_attributes', $attributes ); } // Sales and prices if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { // Variable and grouped products have no prices update_post_meta( $product_id, '_regular_price', '' ); update_post_meta( $product_id, '_sale_price', '' ); update_post_meta( $product_id, '_sale_price_dates_from', '' ); update_post_meta( $product_id, '_sale_price_dates_to', '' ); update_post_meta( $product_id, '_price', '' ); } else { // Regular Price if ( isset( $data['regular_price'] ) ) { $regular_price = ( '' === $data['regular_price'] ) ? '' : wc_format_decimal( $data['regular_price'] ); update_post_meta( $product_id, '_regular_price', $regular_price ); } else { $regular_price = get_post_meta( $product_id, '_regular_price', true ); } // Sale Price if ( isset( $data['sale_price'] ) ) { $sale_price = ( '' === $data['sale_price'] ) ? '' : wc_format_decimal( $data['sale_price'] ); update_post_meta( $product_id, '_sale_price', $sale_price ); } else { $sale_price = get_post_meta( $product_id, '_sale_price', true ); } $date_from = isset( $data['sale_price_dates_from'] ) ? strtotime( $data['sale_price_dates_from'] ) : get_post_meta( $product_id, '_sale_price_dates_from', true ); $date_to = isset( $data['sale_price_dates_to'] ) ? strtotime( $data['sale_price_dates_to'] ) : get_post_meta( $product_id, '_sale_price_dates_to', true ); // Dates if ( $date_from ) { update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); } else { update_post_meta( $product_id, '_sale_price_dates_from', '' ); } if ( $date_to ) { update_post_meta( $product_id, '_sale_price_dates_to', $date_to ); } else { update_post_meta( $product_id, '_sale_price_dates_to', '' ); } if ( $date_to && ! $date_from ) { $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); } // Update price if on sale if ( '' !== $sale_price && '' == $date_to && '' == $date_from ) { update_post_meta( $product_id, '_price', wc_format_decimal( $sale_price ) ); } else { update_post_meta( $product_id, '_price', $regular_price ); } if ( '' !== $sale_price && $date_from && $date_from <= strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $product_id, '_price', wc_format_decimal( $sale_price ) ); } if ( $date_to && $date_to < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $product_id, '_price', $regular_price ); update_post_meta( $product_id, '_sale_price_dates_from', '' ); update_post_meta( $product_id, '_sale_price_dates_to', '' ); } } // Product parent ID for groups if ( isset( $data['parent_id'] ) ) { wp_update_post( array( 'ID' => $product_id, 'post_parent' => absint( $data['parent_id'] ) ) ); } // Update parent if grouped so price sorting works and stays in sync with the cheapest child $_product = wc_get_product( $product_id ); if ( $_product && $_product->post->post_parent > 0 || $product_type == 'grouped' ) { $clear_parent_ids = array(); if ( $_product->post->post_parent > 0 ) { $clear_parent_ids[] = $_product->post->post_parent; } if ( $product_type == 'grouped' ) { $clear_parent_ids[] = $product_id; } if ( ! empty( $clear_parent_ids ) ) { foreach ( $clear_parent_ids as $clear_id ) { $children_by_price = get_posts( array( 'post_parent' => $clear_id, 'orderby' => 'meta_value_num', 'order' => 'asc', 'meta_key' => '_price', 'posts_per_page' => 1, 'post_type' => 'product', 'fields' => 'ids' ) ); if ( $children_by_price ) { foreach ( $children_by_price as $child ) { $child_price = get_post_meta( $child, '_price', true ); update_post_meta( $clear_id, '_price', $child_price ); } } } } } // Sold Individually if ( isset( $data['sold_individually'] ) ) { update_post_meta( $product_id, '_sold_individually', ( $this->is_true( $data['sold_individually'] ) ) ? 'yes' : '' ); } // Stock status if ( isset( $data['in_stock'] ) ) { $stock_status = ( $this->is_true( $data['in_stock'] ) ) ? 'instock' : 'outofstock'; } else { $stock_status = get_post_meta( $product_id, '_stock_status', true ); if ( '' === $stock_status ) { $stock_status = 'instock'; } } // Stock Data if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { // Manage stock if ( isset( $data['managing_stock'] ) ) { $managing_stock = ( $this->is_true( $data['managing_stock'] ) ) ? 'yes' : 'no'; update_post_meta( $product_id, '_manage_stock', $managing_stock ); } else { $managing_stock = get_post_meta( $product_id, '_manage_stock', true ); } // Backorders if ( isset( $data['backorders'] ) ) { if ( 'notify' == $data['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( $this->is_true( $data['backorders'] ) ) ? 'yes' : 'no'; } update_post_meta( $product_id, '_backorders', $backorders ); } else { $backorders = get_post_meta( $product_id, '_backorders', true ); } if ( 'grouped' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } elseif ( 'external' == $product_type ) { update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', 'no' ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, 'instock' ); } elseif ( 'yes' == $managing_stock ) { update_post_meta( $product_id, '_backorders', $backorders ); wc_update_product_stock_status( $product_id, $stock_status ); // Stock quantity if ( isset( $data['stock_quantity'] ) ) { wc_update_product_stock( $product_id, intval( $data['stock_quantity'] ) ); } } else { // Don't manage stock update_post_meta( $product_id, '_manage_stock', 'no' ); update_post_meta( $product_id, '_backorders', $backorders ); update_post_meta( $product_id, '_stock', '' ); wc_update_product_stock_status( $product_id, $stock_status ); } } else { wc_update_product_stock_status( $product_id, $stock_status ); } // Upsells if ( isset( $data['upsell_ids'] ) ) { $upsells = array(); $ids = $data['upsell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $upsells[] = $id; } } update_post_meta( $product_id, '_upsell_ids', $upsells ); } else { delete_post_meta( $product_id, '_upsell_ids' ); } } // Cross sells if ( isset( $data['cross_sell_ids'] ) ) { $crosssells = array(); $ids = $data['cross_sell_ids']; if ( ! empty( $ids ) ) { foreach ( $ids as $id ) { if ( $id && $id > 0 ) { $crosssells[] = $id; } } update_post_meta( $product_id, '_crosssell_ids', $crosssells ); } else { delete_post_meta( $product_id, '_crosssell_ids' ); } } // Product categories if ( isset( $data['categories'] ) ) { $term_ids = array_unique( array_map( 'intval', explode( ',', $data['categories'] ) ) ); wp_set_object_terms( $product_id, $term_ids, 'product_cat' ); } // Product tags if ( isset( $data['tags'] ) ) { $term_ids = array_unique( array_map( 'intval', explode( ',', $data['tags'] ) ) ); wp_set_object_terms( $product_id, $term_ids, 'product_tag' ); } // Downloadable if ( isset( $data['downloadable'] ) ) { $is_downloadable = ( $this->is_true( $data['downloadable'] ) ) ? 'yes' : 'no'; update_post_meta( $product_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $product_id, '_downloadable', true ); } // Downloadable options if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { $this->save_downloadable_files( $product_id, $data['downloads'] ); } // Download limit if ( isset( $data['download_limit'] ) ) { update_post_meta( $product_id, '_download_limit', ( '' === $data['download_limit'] ) ? '' : absint( $data['download_limit'] ) ); } // Download expiry if ( isset( $data['download_expiry'] ) ) { update_post_meta( $product_id, '_download_expiry', ( '' === $data['download_expiry'] ) ? '' : absint( $data['download_expiry'] ) ); } // Download type if ( isset( $data['download_type'] ) ) { update_post_meta( $product_id, '_download_type', wc_clean( $data['download_type'] ) ); } } // Product url if ( $product_type == 'external' ) { if ( isset( $data['product_url'] ) ) { update_post_meta( $product_id, '_product_url', wc_clean( $data['product_url'] ) ); } if ( isset( $data['button_text'] ) ) { update_post_meta( $product_id, '_button_text', wc_clean( $data['button_text'] ) ); } } // Reviews allowed if ( isset( $data['reviews_allowed'] ) ) { $reviews_allowed = ( $this->is_true( $data['reviews_allowed'] ) ) ? 'open' : 'closed'; $wpdb->update( $wpdb->posts, array( 'comment_status' => $reviews_allowed ), array( 'ID' => $product_id ) ); } // Do action for product type do_action( 'woocommerce_cli_process_product_meta_' . $product_type, $product_id, $data ); return true; } /** * Save variations. * * @since 2.5.0 * @param int $id * @param array $data * @return bool * @throws WC_CLI_Exception */ private function save_variations( $id, $data ) { global $wpdb; $variations = $data['variations']; $attributes = (array) maybe_unserialize( get_post_meta( $id, '_product_attributes', true ) ); foreach ( $variations as $menu_order => $variation ) { $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; // Generate a useful post title $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $id ) ) ); // Update or Add post if ( ! $variation_id ) { $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; $new_variation = array( 'post_title' => $variation_post_title, 'post_content' => '', 'post_status' => $post_status, 'post_author' => get_current_user_id(), 'post_parent' => $id, 'post_type' => 'product_variation', 'menu_order' => $menu_order ); $variation_id = wp_insert_post( $new_variation ); do_action( 'woocommerce_create_product_variation', $variation_id ); } else { $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); if ( isset( $variation['visible'] ) ) { $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; $update_variation['post_status'] = $post_status; } $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); do_action( 'woocommerce_update_product_variation', $variation_id ); } // Stop with we don't have a variation ID if ( is_wp_error( $variation_id ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_save_product_variation', $variation_id->get_error_message() ); } // SKU if ( isset( $variation['sku'] ) ) { $sku = get_post_meta( $variation_id, '_sku', true ); $new_sku = wc_clean( $variation['sku'] ); if ( '' == $new_sku ) { update_post_meta( $variation_id, '_sku', '' ); } elseif ( $new_sku !== $sku ) { if ( ! empty( $new_sku ) ) { $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); if ( ! $unique_sku ) { throw new WC_CLI_Exception( 'woocommerce_cli_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ) ); } else { update_post_meta( $variation_id, '_sku', $new_sku ); } } else { update_post_meta( $variation_id, '_sku', '' ); } } } // Thumbnail if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { $image = current( $variation['image'] ); if ( $image && is_array( $image ) ) { if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { $upload = $this->upload_product_image( wc_clean( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_upload_product_image', $upload->get_error_message() ); } $attachment_id = $this->set_product_image_as_attachment( $upload, $id ); update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); } } else { delete_post_meta( $variation_id, '_thumbnail_id' ); } } // Virtual variation if ( isset( $variation['virtual'] ) ) { $is_virtual = ( $this->is_true( $variation['virtual'] ) ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_virtual', $is_virtual ); } // Downloadable variation if ( isset( $variation['downloadable'] ) ) { $is_downloadable = ( $this->is_true( $variation['downloadable'] ) ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_downloadable', $is_downloadable ); } else { $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); } // Shipping data $this->save_product_shipping_data( $variation_id, $variation ); // Stock handling if ( isset( $variation['managing_stock'] ) ) { $managing_stock = ( $this->is_true( $variation['managing_stock'] ) ) ? 'yes' : 'no'; update_post_meta( $variation_id, '_manage_stock', $managing_stock ); } else { $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); } // Only update stock status to user setting if changed by the user, but do so before looking at stock levels at variation level if ( isset( $variation['in_stock'] ) ) { $stock_status = ( $this->is_true( $variation['in_stock'] ) ) ? 'instock' : 'outofstock'; wc_update_product_stock_status( $variation_id, $stock_status ); } if ( 'yes' === $managing_stock ) { if ( isset( $variation['backorders'] ) ) { if ( 'notify' === $variation['backorders'] ) { $backorders = 'notify'; } else { $backorders = ( $this->is_true( $variation['backorders'] ) ) ? 'yes' : 'no'; } } else { $backorders = 'no'; } update_post_meta( $variation_id, '_backorders', $backorders ); if ( isset( $variation['stock_quantity'] ) ) { wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); } } else { delete_post_meta( $variation_id, '_backorders' ); delete_post_meta( $variation_id, '_stock' ); } // Regular Price if ( isset( $variation['regular_price'] ) ) { $regular_price = ( '' === $variation['regular_price'] ) ? '' : wc_format_decimal( $variation['regular_price'] ); update_post_meta( $variation_id, '_regular_price', $regular_price ); } else { $regular_price = get_post_meta( $variation_id, '_regular_price', true ); } // Sale Price if ( isset( $variation['sale_price'] ) ) { $sale_price = ( '' === $variation['sale_price'] ) ? '' : wc_format_decimal( $variation['sale_price'] ); update_post_meta( $variation_id, '_sale_price', $sale_price ); } else { $sale_price = get_post_meta( $variation_id, '_sale_price', true ); } $date_from = isset( $variation['sale_price_dates_from'] ) ? strtotime( $variation['sale_price_dates_from'] ) : get_post_meta( $variation_id, '_sale_price_dates_from', true ); $date_to = isset( $variation['sale_price_dates_to'] ) ? strtotime( $variation['sale_price_dates_to'] ) : get_post_meta( $variation_id, '_sale_price_dates_to', true ); // Save Dates if ( $date_from ) { update_post_meta( $variation_id, '_sale_price_dates_from', $date_from ); } else { update_post_meta( $variation_id, '_sale_price_dates_from', '' ); } if ( $date_to ) { update_post_meta( $variation_id, '_sale_price_dates_to', $date_to ); } else { update_post_meta( $variation_id, '_sale_price_dates_to', '' ); } if ( $date_to && ! $date_from ) { update_post_meta( $variation_id, '_sale_price_dates_from', strtotime( 'NOW', current_time( 'timestamp' ) ) ); } // Update price if on sale if ( '' != $sale_price && '' == $date_to && '' == $date_from ) { update_post_meta( $variation_id, '_price', $sale_price ); } else { update_post_meta( $variation_id, '_price', $regular_price ); } if ( '' != $sale_price && $date_from && $date_from < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $variation_id, '_price', $sale_price ); } if ( $date_to && $date_to < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { update_post_meta( $variation_id, '_price', $regular_price ); update_post_meta( $variation_id, '_sale_price_dates_from', '' ); update_post_meta( $variation_id, '_sale_price_dates_to', '' ); } // Tax class if ( isset( $variation['tax_class'] ) ) { if ( $variation['tax_class'] !== 'parent' ) { update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); } else { delete_post_meta( $variation_id, '_tax_class' ); } } // Downloads if ( 'yes' == $is_downloadable ) { // Downloadable files if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { $this->save_downloadable_files( $id, $variation['downloads'], $variation_id ); } // Download limit if ( isset( $variation['download_limit'] ) ) { $download_limit = absint( $variation['download_limit'] ); update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); } // Download expiry if ( isset( $variation['download_expiry'] ) ) { $download_expiry = absint( $variation['download_expiry'] ); update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); } } else { update_post_meta( $variation_id, '_download_limit', '' ); update_post_meta( $variation_id, '_download_expiry', '' ); update_post_meta( $variation_id, '_downloadable_files', '' ); } // Update taxonomies if ( isset( $variation['attributes'] ) ) { $updated_attribute_keys = array(); foreach ( $variation['attributes'] as $slug => $value ) { $taxonomy = sanitize_title( $slug ); $_attribute = array(); if ( $this->get_attribute_taxonomy_by_slug( $slug ) !== null ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $slug ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; } if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { $attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); $attribute_value = ! empty( $value ) ? sanitize_title( stripslashes( $value ) ) : ''; $updated_attribute_keys[] = $attribute_key; update_post_meta( $variation_id, $attribute_key, $attribute_value ); } } // Remove old taxonomies attributes so data is kept up to date - first get attribute key names $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); foreach ( $delete_attribute_keys as $key ) { delete_post_meta( $variation_id, $key ); } } do_action( 'woocommerce_cli_save_product_variation', $variation_id, $menu_order, $variation ); } // Update parent if variable so price sorting works and stays in sync with the cheapest child WC_Product_Variable::sync( $id ); // Update default attributes options setting if ( isset( $data['default_attribute'] ) ) { $data['default_attributes'] = $data['default_attribute']; } if ( isset( $data['default_attributes'] ) && is_array( $data['default_attributes'] ) ) { $default_attributes = array(); foreach ( $data['default_attributes'] as $default_attr_key => $default_attr ) { if ( ! isset( $default_attr['name'] ) ) { continue; } $taxonomy = sanitize_title( $default_attr['name'] ); if ( isset( $default_attr['slug'] ) ) { $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); } if ( isset( $attributes[ $taxonomy ] ) ) { $_attribute = $attributes[ $taxonomy ]; if ( $_attribute['is_variation'] ) { // Don't use wc_clean as it destroys sanitized characters if ( isset( $default_attr['option'] ) ) { $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); } else { $value = ''; } if ( $value ) { $default_attributes[ $taxonomy ] = $value; } } } } update_post_meta( $id, '_default_attributes', $default_attributes ); } return true; } /** * Save product images. * * @since 2.5.0 * @param array $images * @param int $id * @throws WC_CLI_Exception */ private function save_product_images( $id, $images ) { if ( is_array( $images ) ) { $gallery = array(); foreach ( $images as $image ) { if ( isset( $image['position'] ) && $image['position'] == 0 ) { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_upload_product_image', $upload->get_error_message() ); } $attachment_id = $this->set_product_image_as_attachment( $upload, $id ); } set_post_thumbnail( $id, $attachment_id ); } else { $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; if ( 0 === $attachment_id && isset( $image['src'] ) ) { $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); if ( is_wp_error( $upload ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_upload_product_image', $upload->get_error_message() ); } $gallery[] = $this->set_product_image_as_attachment( $upload, $id ); } else { $gallery[] = $attachment_id; } } } if ( ! empty( $gallery ) ) { update_post_meta( $id, '_product_image_gallery', implode( ',', $gallery ) ); } } else { delete_post_thumbnail( $id ); update_post_meta( $id, '_product_image_gallery', '' ); } } /** * Save product shipping data. * * @since 2.5.0 * @param int $id * @param array $data */ private function save_product_shipping_data( $id, $data ) { if ( isset( $data['weight'] ) ) { update_post_meta( $id, '_weight', ( '' === $data['weight'] ) ? '' : wc_format_decimal( $data['weight'] ) ); } // Product dimensions if ( isset( $data['dimensions'] ) ) { // Height if ( isset( $data['dimensions']['height'] ) ) { update_post_meta( $id, '_height', ( '' === $data['dimensions']['height'] ) ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); } // Width if ( isset( $data['dimensions']['width'] ) ) { update_post_meta( $id, '_width', ( '' === $data['dimensions']['width'] ) ? '' : wc_format_decimal($data['dimensions']['width'] ) ); } // Length if ( isset( $data['dimensions']['length'] ) ) { update_post_meta( $id, '_length', ( '' === $data['dimensions']['length'] ) ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); } } // Virtual if ( isset( $data['virtual'] ) ) { $virtual = ( $this->is_true( $data['virtual'] ) ) ? 'yes' : 'no'; if ( 'yes' == $virtual ) { update_post_meta( $id, '_weight', '' ); update_post_meta( $id, '_length', '' ); update_post_meta( $id, '_width', '' ); update_post_meta( $id, '_height', '' ); } } // Shipping class if ( isset( $data['shipping_class'] ) ) { wp_set_object_terms( $id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); } } /** * Save downloadable files. * * @since 2.5.0 * @param int $product_id * @param array $downloads * @param int $variation_id */ private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { $files = array(); // File paths will be stored in an array keyed off md5(file path) foreach ( $downloads as $key => $file ) { if ( isset( $file['url'] ) ) { $file['file'] = $file['url']; } if ( ! isset( $file['file'] ) ) { continue; } $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; if ( 0 === strpos( $file['file'], 'http' ) ) { $file_url = esc_url_raw( $file['file'] ); } else { $file_url = wc_clean( $file['file'] ); } $files[ md5( $file_url ) ] = array( 'name' => $file_name, 'file' => $file_url ); } // Grant permission to any newly added files on any existing orders for this product prior to saving do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); $id = ( 0 === $variation_id ) ? $product_id : $variation_id; update_post_meta( $id, '_downloadable_files', $files ); } /** * Get attribute taxonomy by slug. * * @since 2.5.0 * @param string $slug * @return string|null */ private function get_attribute_taxonomy_by_slug( $slug ) { $taxonomy = null; $attribute_taxonomies = wc_get_attribute_taxonomies(); foreach ( $attribute_taxonomies as $key => $tax ) { if ( $slug == $tax->attribute_name ) { $taxonomy = 'pa_' . $tax->attribute_name; break; } } return $taxonomy; } /** * Returns image mime types users are allowed to upload via the API. * @since 2.6.4 * @return array */ private function allowed_image_mime_types() { return apply_filters( 'woocommerce_cli_allowed_image_mime_types', array( 'jpg|jpeg|jpe' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'bmp' => 'image/bmp', 'tiff|tif' => 'image/tiff', 'ico' => 'image/x-icon', ) ); } /** * Upload image from URL * * @since 2.5.0 * @param string $image_url * @return int|WP_Error attachment id * @throws WC_CLI_Exception */ private function upload_product_image( $image_url ) { $file_name = basename( current( explode( '?', $image_url ) ) ); $parsed_url = @parse_url( $image_url ); // Check parsed URL if ( ! $parsed_url || ! is_array( $parsed_url ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_image', sprintf( __( 'Invalid URL %s', 'woocommerce' ), $image_url ) ); } // Ensure url is valid $image_url = str_replace( ' ', '%20', $image_url ); // Get the file $response = wp_safe_remote_get( $image_url, array( 'timeout' => 10 ) ); if ( is_wp_error( $response ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' . sprintf( __( 'Error: %s.', 'woocommerce' ), $response->get_error_message() ) ); } elseif ( 200 !== wp_remote_retrieve_response_code( $response ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_remote_product_image', sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) ); } // Ensure we have a file name and type $wp_filetype = wp_check_filetype( $file_name, $this->allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { $headers = wp_remote_retrieve_headers( $response ); if ( isset( $headers['content-disposition'] ) && strstr( $headers['content-disposition'], 'filename=' ) ) { $disposition = end( explode( 'filename=', $headers['content-disposition'] ) ); $disposition = sanitize_file_name( $disposition ); $file_name = $disposition; } elseif ( isset( $headers['content-type'] ) && strstr( $headers['content-type'], 'image/' ) ) { $file_name = 'image.' . str_replace( 'image/', '', $headers['content-type'] ); } unset( $headers ); // Recheck filetype $wp_filetype = wp_check_filetype( $file_name, $this->allowed_image_mime_types() ); if ( ! $wp_filetype['type'] ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_image_type', __( 'Invalid image type.', 'woocommerce' ) ); } } // Upload the file. $upload = wp_upload_bits( $file_name, '', wp_remote_retrieve_body( $response ) ); if ( $upload['error'] ) { throw new WC_CLI_Exception( 'woocommerce_cli_product_image_upload_error', $upload['error'] ); } // Get filesize $filesize = filesize( $upload['file'] ); if ( 0 == $filesize ) { @unlink( $upload['file'] ); unset( $upload ); throw new WC_CLI_Exception( 'woocommerce_cli_product_image_upload_file_error', __( 'Zero size file downloaded', 'woocommerce' ) ); } unset( $response ); return $upload; } /** * Sets product image as attachment and returns the attachment ID. * * @since 2.5.0 * @param array $upload * @param int $id * @return int */ private function set_product_image_as_attachment( $upload, $id ) { $info = wp_check_filetype( $upload['file'] ); $title = ''; $content = ''; if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $title = wc_clean( $image_meta['title'] ); } if ( trim( $image_meta['caption'] ) ) { $content = wc_clean( $image_meta['caption'] ); } } $attachment = array( 'post_mime_type' => $info['type'], 'guid' => $upload['url'], 'post_parent' => $id, 'post_title' => $title, 'post_content' => $content ); $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); if ( ! is_wp_error( $attachment_id ) ) { wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); } return $attachment_id; } /** * Clear product * * @since 2.5.0 * @param int $product_id Product ID */ private function clear_product( $product_id ) { if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { return; } // Delete product attachments $attachments = get_children( array( 'post_parent' => $product_id, 'post_status' => 'any', 'post_type' => 'attachment', ) ); foreach ( (array) $attachments as $attachment ) { wp_delete_attachment( $attachment->ID, true ); } // Delete product wp_delete_post( $product_id, true ); } /** * Check if is a variable. * * @since 2.6.2 * @param array $data * @return bool */ private function is_variable( $data ) { if ( isset( $data['type'] ) && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { $types = apply_filters( 'woocommerce_cli_get_product_variable_types', array( 'variable' ) ); return in_array( $data['type'], $types ); } return false; } } class-wc-cli-command.php000066600000026401152140537160011165 0ustar00 'post_date_gmt', 'after' => $this->parse_datetime( $assoc_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $assoc_args['created_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->parse_datetime( $assoc_args['created_at_max'] ), 'inclusive' => true ); } // resources updated after specified date if ( ! empty( $assoc_args['updated_at_min'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->parse_datetime( $assoc_args['updated_at_min'] ), 'inclusive' => true ); } // resources updated before specified date if ( ! empty( $assoc_args['updated_at_max'] ) ) { $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->parse_datetime( $assoc_args['updated_at_max'] ), 'inclusive' => true ); } } // Search. if ( ! empty( $assoc_args['q'] ) ) { $args['s'] = $assoc_args['q']; } // Number of post to show per page. if ( ! empty( $assoc_args['limit'] ) ) { $args['posts_per_page'] = $assoc_args['limit']; } // Number of post to displace or pass over. if ( ! empty( $assoc_args['offset'] ) ) { $args['offset'] = $assoc_args['offset']; } // order (ASC or DESC, DESC by default). if ( ! empty( $assoc_args['order'] ) ) { $args['order'] = $assoc_args['order']; } // orderby. if ( ! empty( $assoc_args['orderby'] ) ) { $args['orderby'] = $assoc_args['orderby']; // allow sorting by meta value if ( ! empty( $assoc_args['orderby_meta_key'] ) ) { $args['meta_key'] = $assoc_args['orderby_meta_key']; } } // allow post status change if ( ! empty( $assoc_args['post_status'] ) ) { $args['post_status'] = $assoc_args['post_status']; unset( $assoc_args['post_status'] ); } // filter by a list of post ids if ( ! empty( $assoc_args['in'] ) ) { $args['post__in'] = explode( ',', $assoc_args['in'] ); unset( $assoc_args['in'] ); } // exclude by a list of post ids if ( ! empty( $assoc_args['not_in'] ) ) { $args['post__not_in'] = explode( ',', $assoc_args['not_in'] ); unset( $assoc_args['not_in'] ); } // posts page. $args['paged'] = ( isset( $assoc_args['page'] ) ) ? absint( $assoc_args['page'] ) : 1; $args = apply_filters( 'woocommerce_cli_query_args', $args, $assoc_args ); return array_merge( $base_args, $args ); } /** * Add common cli arguments to argument list before WP_User_Query is run. * * @since 2.5.0 * @param array $base_args required arguments for the query (e.g. `post_type`, etc) * @param array $assoc_args arguments provided in when invoking the command * @return array */ protected function merge_wp_user_query_args( $base_args, $assoc_args ) { $args = array(); // Custom Role if ( ! empty( $assoc_args['role'] ) ) { $args['role'] = $assoc_args['role']; } // Search if ( ! empty( $assoc_args['q'] ) ) { $args['search'] = $assoc_args['q']; } // Limit number of users returned. if ( ! empty( $assoc_args['limit'] ) ) { $args['number'] = absint( $assoc_args['limit'] ); } // Offset if ( ! empty( $assoc_args['offset'] ) ) { $args['offset'] = absint( $assoc_args['offset'] ); } // date if ( ! empty( $assoc_args['created_at_min'] ) || ! empty( $assoc_args['created_at_max'] ) ) { $args['date_query'] = array(); // resources created after specified date if ( ! empty( $assoc_args['created_at_min'] ) ) { $args['date_query'][] = array( 'after' => $this->parse_datetime( $assoc_args['created_at_min'] ), 'inclusive' => true ); } // resources created before specified date if ( ! empty( $assoc_args['created_at_max'] ) ) { $args['date_query'][] = array( 'before' => $this->parse_datetime( $assoc_args['created_at_max'] ), 'inclusive' => true ); } } // Order (ASC or DESC, ASC by default). if ( ! empty( $assoc_args['order'] ) ) { $args['order'] = $assoc_args['order']; } // Orderby. if ( ! empty( $assoc_args['orderby'] ) ) { $args['orderby'] = $assoc_args['orderby']; } $args = apply_filters( 'woocommerce_cli_user_query_args', $args, $assoc_args ); return array_merge( $base_args, $args ); } /** * Parse an RFC3339 datetime into a MySQl datetime. * * Invalid dates default to unix epoch. * * @since 2.5.0 * @param string $datetime RFC3339 datetime * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) */ protected function parse_datetime( $datetime ) { // Strip millisecond precision (a full stop followed by one or more digits) if ( strpos( $datetime, '.' ) !== false ) { $datetime = preg_replace( '/\.\d+/', '', $datetime ); } // default timezone to UTC $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); try { $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); } catch ( Exception $e ) { $datetime = new DateTime( '@0' ); } return $datetime->format( 'Y-m-d H:i:s' ); } /** * Format a unix timestamp or MySQL datetime into an RFC3339 datetime. * * @since 2.5.0 * @param int|string $timestamp unix timestamp or MySQL datetime * @param bool $convert_to_utc * @return string RFC3339 datetime */ protected function format_datetime( $timestamp, $convert_to_utc = false ) { if ( $convert_to_utc ) { $timezone = new DateTimeZone( wc_timezone_string() ); } else { $timezone = new DateTimeZone( 'UTC' ); } try { if ( is_numeric( $timestamp ) ) { $date = new DateTime( "@{$timestamp}" ); } else { $date = new DateTime( $timestamp, $timezone ); } // convert to UTC by adjusting the time based on the offset of the site's timezone if ( $convert_to_utc ) { $date->modify( -1 * $date->getOffset() . ' seconds' ); } } catch ( Exception $e ) { $date = new DateTime( '@0' ); } return $date->format( 'Y-m-d\TH:i:s\Z' ); } /** * Get formatter object based on supplied arguments. * * @since 2.5.0 * @param array $assoc_args Associative args from CLI to determine formatting * @return \WP_CLI\Formatter */ protected function get_formatter( $assoc_args ) { $args = $this->get_format_args( $assoc_args ); return new \WP_CLI\Formatter( $args ); } /** * Get default fields for formatter. * * Class that extends WC_CLI_Command should override this method. * * @since 2.5.0 * @return null|string|array */ protected function get_default_format_fields() { return null; } /** * Get format args that will be passed into CLI Formatter. * * @since 2.5.0 * @param array $assoc_args Associative args from CLI * @return array Formatter args */ protected function get_format_args( $assoc_args ) { $format_args = array( 'fields' => $this->get_default_format_fields(), 'field' => null, 'format' => 'table', ); if ( isset( $assoc_args['fields'] ) ) { $format_args['fields'] = $assoc_args['fields']; } if ( isset( $assoc_args['field'] ) ) { $format_args['field'] = $assoc_args['field']; } if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], array( 'count', 'ids', 'table', 'csv', 'json' ) ) ) { $format_args['format'] = $assoc_args['format']; } return $format_args; } /** * Flatten multidimensional array in which nested array will be prefixed with * parent keys separated with dot char, e.g. given an array: * * array( * 'a' => array( * 'b' => array( * 'c' => ... * ) * ) * ) * * a flatten array would contain key 'a.b.c' => ... * * @since 2.5.0 * @param array $arr Array that may contains nested array * @param string $prefix Prefix * * @return array Flattened array */ protected function flatten_array( $arr, $prefix = '' ) { $flattened = array(); foreach ( $arr as $key => $value ) { if ( is_array( $value ) ) { if ( sizeof( $value ) > 0 ) { // Full access to whole elements if indices are numerical. $flattened[ $prefix . $key ] = $value; // This is naive assumption that if element with index zero // exists then array indices are numberical. if ( ! empty( $value[0] ) ) { // Allow size of array to be accessed, i.e., a.b.arr.size $flattened[ $prefix . $key . '.size' ] = sizeof( $value ); } $flattened = array_merge( $flattened, $this->flatten_array( $value, $prefix . $key . '.' ) ); } else { $flattened[ $prefix . $key ] = ''; // Tells user that size of this array is zero. $flattened[ $prefix . $key . '.size' ] = 0; } } else { $flattened[ $prefix . $key ] = $value; } } return $flattened; } /** * Unflatten array will make key 'a.b.c' becomes nested array: * * array( * 'a' => array( * 'b' => array( * 'c' => ... * ) * ) * ) * * @since 2.5.0 * @param array $arr Flattened array * @return array */ protected function unflatten_array( $arr ) { $unflatten = array(); foreach ( $arr as $key => $value ) { $key_list = explode( '.', $key ); $first_key = array_shift( $key_list ); $first_key = $this->get_normalized_array_key( $first_key ); if ( sizeof( $key_list ) > 0 ) { $remaining_keys = implode( '.', $key_list ); $subarray = $this->unflatten_array( array( $remaining_keys => $value ) ); foreach ( $subarray as $sub_key => $sub_value ) { $sub_key = $this->get_normalized_array_key( $sub_key ); if ( ! empty( $unflatten[ $first_key ][ $sub_key ] ) ) { $unflatten[ $first_key ][ $sub_key ] = array_merge_recursive( $unflatten[ $first_key ][ $sub_key ], $sub_value ); } else { $unflatten[ $first_key ][ $sub_key ] = $sub_value; } } } else { $unflatten[ $first_key ] = $value; } } return $unflatten; } /** * Get normalized array key. If key is a numeric one it will be converted * as absolute integer. * * @since 2.5.0 * @param string $key Array key * @return string|int */ protected function get_normalized_array_key( $key ) { if ( is_numeric( $key ) ) { $key = absint( $key ); } return $key; } /** * Check if the value is equal to 'yes', 'true' or '1' * * @since 2.5.4 * @param string $value * @return boolean */ protected function is_true( $value ) { return ( 'yes' === $value || 'true' === $value || '1' === $value ) ? true : false; } } class-wc-cli-coupon.php000066600000053761152140537160011063 0ustar00=] * : Associative args for the new coupon. * * [--porcelain] * : Outputs just the new coupon id. * * ## AVAILABLE FIELDS * * These fields are available for create command: * * * code * * type * * amount * * description * * expiry_date * * individual_use * * product_ids * * exclude_product_ids * * usage_limit * * usage_limit_per_user * * limit_usage_to_x_items * * usage_count * * enable_free_shipping * * product_category_ids * * exclude_product_category_ids * * minimum_amount * * maximum_amount * * customer_emails * * ## EXAMPLES * * wp wc coupon create --code=new-coupon --type=percent * */ public function create( $__, $assoc_args ) { global $wpdb; try { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $assoc_args = apply_filters( 'woocommerce_cli_create_coupon_data', $assoc_args ); // Check if coupon code is specified. if ( ! isset( $assoc_args['code'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_missing_coupon_code', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'code' ) ); } $coupon_code = apply_filters( 'woocommerce_coupon_code', $assoc_args['code'] ); // Check for duplicate coupon codes. $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' ", $coupon_code ) ); if ( $coupon_found ) { throw new WC_CLI_Exception( 'woocommerce_cli_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ) ); } $defaults = array( 'type' => 'fixed_cart', 'amount' => 0, 'individual_use' => false, 'product_ids' => array(), 'exclude_product_ids' => array(), 'usage_limit' => '', 'usage_limit_per_user' => '', 'limit_usage_to_x_items' => '', 'usage_count' => '', 'expiry_date' => '', 'enable_free_shipping' => false, 'product_category_ids' => array(), 'exclude_product_category_ids' => array(), 'exclude_sale_items' => false, 'minimum_amount' => '', 'maximum_amount' => '', 'customer_emails' => array(), 'description' => '' ); $coupon_data = wp_parse_args( $assoc_args, $defaults ); // Validate coupon types if ( ! in_array( wc_clean( $coupon_data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ) ); } $new_coupon = array( 'post_title' => $coupon_code, 'post_content' => '', 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_type' => 'shop_coupon', 'post_excerpt' => $coupon_data['description'] ); $id = wp_insert_post( $new_coupon, $wp_error = false ); if ( is_wp_error( $id ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_create_coupon', $id->get_error_message() ); } // Set coupon meta update_post_meta( $id, 'discount_type', $coupon_data['type'] ); update_post_meta( $id, 'coupon_amount', wc_format_decimal( $coupon_data['amount'] ) ); update_post_meta( $id, 'individual_use', ( $this->is_true( $coupon_data['individual_use'] ) ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['product_ids'] ) ) ) ); update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $coupon_data['exclude_product_ids'] ) ) ) ); update_post_meta( $id, 'usage_limit', absint( $coupon_data['usage_limit'] ) ); update_post_meta( $id, 'usage_limit_per_user', absint( $coupon_data['usage_limit_per_user'] ) ); update_post_meta( $id, 'limit_usage_to_x_items', absint( $coupon_data['limit_usage_to_x_items'] ) ); update_post_meta( $id, 'usage_count', absint( $coupon_data['usage_count'] ) ); update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $coupon_data['expiry_date'] ) ) ); update_post_meta( $id, 'free_shipping', ( $this->is_true( $coupon_data['enable_free_shipping'] ) ) ? 'yes' : 'no' ); update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $coupon_data['product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $coupon_data['exclude_product_category_ids'] ) ) ); update_post_meta( $id, 'exclude_sale_items', ( $this->is_true( $coupon_data['exclude_sale_items'] ) ) ? 'yes' : 'no' ); update_post_meta( $id, 'minimum_amount', wc_format_decimal( $coupon_data['minimum_amount'] ) ); update_post_meta( $id, 'maximum_amount', wc_format_decimal( $coupon_data['maximum_amount'] ) ); update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $coupon_data['customer_emails'] ) ) ); do_action( 'woocommerce_cli_create_coupon', $id, $coupon_data ); if ( $porcelain ) { WP_CLI::line( $id ); } else { WP_CLI::success( "Created coupon $id." ); } } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Delete one or more coupons. * * ## OPTIONS * * ... * : The coupon ID to delete. * * ## EXAMPLES * * wp wc coupon delete 123 * * wp wc coupon delete $(wp wc coupon list --format=ids) * */ public function delete( $args, $assoc_args ) { $exit_code = 0; foreach ( $this->get_many_coupons_from_ids_or_codes( $args, true ) as $coupon ) { do_action( 'woocommerce_cli_delete_coupon', $coupon->id ); $r = wp_delete_post( $coupon->id, true ); if ( $r ) { WP_CLI::success( "Deleted coupon {$coupon->id}." ); } else { $exit_code += 1; WP_CLI::warning( "Failed deleting coupon {$coupon->id}." ); } } exit( $exit_code ? 1 : 0 ); } /** * Get a coupon. * * ## OPTIONS * * * : Coupon ID or code * * [--field=] * : Instead of returning the whole coupon fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the coupon's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * These fields are available for get command: * * * id * * code * * type * * amount * * description * * expiry_date * * individual_use * * product_ids * * exclude_product_ids * * usage_limit * * usage_limit_per_user * * limit_usage_to_x_items * * usage_count * * enable_free_shipping * * product_category_ids * * exclude_product_category_ids * * minimum_amount * * maximum_amount * * customer_emails * * ## EXAMPLES * * wp wc coupon get 123 --field=discount_type * * wp wc coupon get disc50 --format=json > disc50.json * * @since 2.5.0 */ public function get( $args, $assoc_args ) { global $wpdb; try { $coupon = $this->get_coupon_from_id_or_code( $args[0] ); if ( ! $coupon ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_coupon', sprintf( __( 'Invalid coupon ID or code: %s', 'woocommerce' ), $args[0] ) ); } $coupon_post = get_post( $coupon->id ); $coupon_data = array( 'id' => $coupon->id, 'code' => $coupon->code, 'type' => $coupon->type, 'created_at' => $this->format_datetime( $coupon_post->post_date_gmt ), 'updated_at' => $this->format_datetime( $coupon_post->post_modified_gmt ), 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), 'individual_use' => $coupon->individual_use, 'product_ids' => $coupon_post->product_ids, 'exclude_product_ids' => $coupon_post->exclude_product_ids, 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, 'usage_count' => (int) $coupon->usage_count, 'expiry_date' => ( ! empty( $coupon->expiry_date ) ) ? $this->format_datetime( $coupon->expiry_date ) : null, 'enable_free_shipping' => $coupon->free_shipping, 'product_category_ids' => implode( ', ', $coupon->product_categories ), 'exclude_product_category_ids' => implode( ', ', $coupon->exclude_product_categories ), 'exclude_sale_items' => $coupon->exclude_sale_items, 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), 'customer_emails' => implode( ', ', $coupon->customer_email ), 'description' => $coupon_post->post_excerpt, ); $coupon_data = apply_filters( 'woocommerce_cli_get_coupon', $coupon_data ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $coupon_data ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $coupon_data ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List coupons. * * ## OPTIONS * * [--=] * : Filter coupon based on coupon property. * * [--field=] * : Prints the value of a single field for each coupon. * * [--fields=] * : Limit the output to specific coupon fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each coupon: * * * id * * code * * type * * amount * * description * * expiry_date * * These fields are optionally available: * * * individual_use * * product_ids * * exclude_product_ids * * usage_limit * * usage_limit_per_user * * limit_usage_to_x_items * * usage_count * * free_shipping * * product_category_ids * * exclude_product_category_ids * * exclude_sale_items * * minimum_amount * * maximum_amount * * customer_emails * * Fields for filtering query result also available: * * * q Filter coupons with search query. * * in Specify coupon IDs to retrieve. * * not_in Specify coupon IDs NOT to retrieve. * * created_at_min Filter coupons created after this date. * * created_at_max Filter coupons created before this date. * * updated_at_min Filter coupons updated after this date. * * updated_at_max Filter coupons updated before this date. * * page Page number. * * offset Number of coupon to displace or pass over. * * order Accepted values: ASC and DESC. Default: DESC. * * orderby Sort retrieved coupons by parameter. One or more options can be passed. * * ## EXAMPLES * * wp wc coupon list * * wp wc coupon list --field=id * * wp wc coupon list --fields=id,code,type --format=json * * @since 2.5.0 * @subcommand list */ public function list_( $__, $assoc_args ) { $query_args = $this->merge_wp_query_args( $this->get_list_query_args(), $assoc_args ); $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_Query( $query_args ); echo implode( ' ', $query->posts ); } else { $query = new WP_Query( $query_args ); $items = $this->format_posts_to_items( $query->posts ); $formatter->display_items( $items ); } } /** * Get coupon types. * * ## EXAMPLES * * wp wc coupon types * * @since 2.5.0 */ public function types( $__, $___ ) { $coupon_types = wc_get_coupon_types(); foreach ( $coupon_types as $type => $label ) { WP_CLI::line( sprintf( '%s: %s', $label, $type ) ); } } /** * Update one or more coupons. * * ## OPTIONS * * * : The ID or code of the coupon to update. * * [--=] * : One or more fields to update. * * ## AVAILABLE FIELDS * * These fields are available for update command: * * * code * * type * * amount * * description * * expiry_date * * individual_use * * product_ids * * exclude_product_ids * * usage_limit * * usage_limit_per_user * * limit_usage_to_x_items * * usage_count * * enable_free_shipping * * product_category_ids * * exclude_product_categories * * exclude_product_category_ids * * minimum_amount * * maximum_amount * * customer_emails * * ## EXAMPLES * * wp wc coupon update 123 --amount=5 * * wp wc coupon update coupon-code --code=new-coupon-code * * @since 2.5.0 */ public function update( $args, $assoc_args ) { try { $coupon = $this->get_coupon_from_id_or_code( $args[0] ); if ( ! $coupon ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_coupon', sprintf( __( 'Invalid coupon ID or code: %s', 'woocommerce' ), $args[0] ) ); } $id = $coupon->id; $coupon_code = $coupon->code; $data = apply_filters( 'woocommerce_cli_update_coupon_data', $assoc_args, $id ); if ( isset( $data['code'] ) ) { global $wpdb; $coupon_code = apply_filters( 'woocommerce_coupon_code', $data['code'] ); // Check for duplicate coupon codes $coupon_found = $wpdb->get_var( $wpdb->prepare( " SELECT $wpdb->posts.ID FROM $wpdb->posts WHERE $wpdb->posts.post_type = 'shop_coupon' AND $wpdb->posts.post_status = 'publish' AND $wpdb->posts.post_title = '%s' AND $wpdb->posts.ID != %s ", $coupon_code, $id ) ); if ( $coupon_found ) { throw new WC_CLI_Exception( 'woocommerce_cli_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ) ); } } $id = wp_update_post( array( 'ID' => intval( $id ), 'post_title' => $coupon_code, 'post_excerpt' => isset( $data['description'] ) ? $data['description'] : '' ) ); if ( 0 === $id ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_update_coupon', __( 'Failed to update coupon', 'woocommerce' ) ); } if ( isset( $data['type'] ) ) { // Validate coupon types. if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_coupon_types() ) ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_coupon_type', sprintf( __( 'Invalid coupon type - the coupon type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_coupon_types() ) ) ) ); } update_post_meta( $id, 'discount_type', $data['type'] ); } if ( isset( $data['amount'] ) ) { update_post_meta( $id, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); } if ( isset( $data['individual_use'] ) ) { update_post_meta( $id, 'individual_use', ( $this->is_true( $data['individual_use'] ) ) ? 'yes' : 'no' ); } if ( isset( $data['product_ids'] ) ) { update_post_meta( $id, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); } if ( isset( $data['exclude_product_ids'] ) ) { update_post_meta( $id, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); } if ( isset( $data['usage_limit'] ) ) { update_post_meta( $id, 'usage_limit', absint( $data['usage_limit'] ) ); } if ( isset( $data['usage_limit_per_user'] ) ) { update_post_meta( $id, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); } if ( isset( $data['limit_usage_to_x_items'] ) ) { update_post_meta( $id, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); } if ( isset( $data['usage_count'] ) ) { update_post_meta( $id, 'usage_count', absint( $data['usage_count'] ) ); } if ( isset( $data['expiry_date'] ) ) { update_post_meta( $id, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); } if ( isset( $data['enable_free_shipping'] ) ) { update_post_meta( $id, 'free_shipping', ( $this->is_true( $data['enable_free_shipping'] ) ) ? 'yes' : 'no' ); } if ( isset( $data['product_category_ids'] ) ) { update_post_meta( $id, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); } if ( isset( $data['exclude_product_category_ids'] ) ) { update_post_meta( $id, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); } if ( isset( $data['exclude_sale_items'] ) ) { update_post_meta( $id, 'exclude_sale_items', ( $this->is_true( $data['exclude_sale_items'] ) ) ? 'yes' : 'no' ); } if ( isset( $data['minimum_amount'] ) ) { update_post_meta( $id, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); } if ( isset( $data['maximum_amount'] ) ) { update_post_meta( $id, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); } if ( isset( $data['customer_emails'] ) ) { update_post_meta( $id, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); } do_action( 'woocommerce_cli_update_coupon', $id, $data ); WP_CLI::success( "Updated coupon $id." ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get query args for list subcommand. * * @since 2.5.0 * @return array */ protected function get_list_query_args() { return array( 'post_type' => 'shop_coupon', 'post_status' => 'publish', 'posts_per_page' => -1, 'order' => 'DESC', ); } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,code,type,amount,description,expiry_date'; } /** * Format posts from WP_Query result to items in which each item contain * common properties of item, for instance `post_title` will be `code`. * * @since 2.5.0 * @param array $posts Array of post * @return array Items */ protected function format_posts_to_items( $posts ) { $items = array(); foreach ( $posts as $post ) { $items[] = array( 'id' => $post->ID, 'code' => $post->post_title, 'type' => $post->discount_type, 'created_at' => $this->format_datetime( $post->post_date_gmt ), 'updated_at' => $this->format_datetime( $post->post_modified_gmt ), 'amount' => wc_format_decimal( $post->coupon_amount, 2 ), 'individual_use' => $post->individual_use, 'product_ids' => $post->product_ids, 'exclude_product_ids' => $post->exclude_product_ids, 'usage_limit' => ( ! empty( $post->usage_limit ) ) ? $post->usage_limit : null, 'usage_limit_per_user' => ( ! empty( $post->usage_limit_per_user ) ) ? $post->usage_limit_per_user : null, 'limit_usage_to_x_items' => (int) $post->limit_usage_to_x_items, 'usage_count' => (int) $post->usage_count, 'expiry_date' => ( ! empty( $post->expiry_date ) ) ? $this->format_datetime( $post->expiry_date ) : null, 'free_shipping' => $post->free_shipping, 'product_category_ids' => implode( ', ', is_array( $post->product_categories ) ? $post->product_categories : array() ), 'exclude_product_category_ids' => implode( ', ', is_array( $post->exclude_product_categories ) ? $post->exclude_product_categories : array() ), 'exclude_sale_items' => $post->exclude_sale_items, 'minimum_amount' => wc_format_decimal( $post->minimum_amount, 2 ), 'maximum_amount' => wc_format_decimal( $post->maximum_amount, 2 ), 'customer_emails' => implode( ', ', is_array( $post->customer_email ) ? $post->customer_email : array() ), 'description' => $post->post_excerpt, ); } return $items; } /** * Get expiry_date format before saved into DB. * * @since 2.5.0 * @param string $expiry_date * @return string */ protected function get_coupon_expiry_date( $expiry_date ) { if ( '' !== $expiry_date ) { return date( 'Y-m-d', strtotime( $expiry_date ) ); } return ''; } /** * Get coupon from coupon's ID or code. * * @since 2.5.0 * @param int|string $coupon_id_or_code Coupon's ID or code * @param bool $display_warning Display warning if ID or code is invalid. Default false. * @return WC_Coupon */ protected function get_coupon_from_id_or_code( $coupon_id_or_code, $display_warning = false ) { global $wpdb; $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE (id = %s OR post_title = %s) AND post_type = 'shop_coupon' AND post_status = 'publish' LIMIT 1", $coupon_id_or_code, $coupon_id_or_code ) ); if ( ! $code ) { if ( $display_warning ) { WP_CLI::warning( "Invalid coupon ID or code $coupon_id_or_code" ); } return null; } return new WC_Coupon( $code ); } /** * Get coupon from coupon's ID or code. * * @since 2.5.0 * @param array $args Coupon's IDs or codes * @param bool $display_warning Display warning if ID or code is invalid. Default false. * @return WC_Coupon */ protected function get_many_coupons_from_ids_or_codes( $args, $display_warning = false ) { $coupons = array(); foreach ( $args as $arg ) { $code = $this->get_coupon_from_id_or_code( $arg, $display_warning ); if ( $code ) { $coupons[] = $code; } } return $coupons; } } class-wc-cli-tax.php000066600000037257152140537160010356 0ustar00=] * : Associative args for the new tax rate. * * [--porcelain] * : Outputs just the new tax rate id. * * ## AVAILABLE FIELDS * * These fields are available for create command: * * * country * * state * * postcode * * city * * rate * * name * * priority * * compound * * shipping * * class * * order * * ## EXAMPLES * * wp wc tax create --country=US --rate=5 --class=standard --type=percent * * @since 2.5.0 */ public function create( $__, $assoc_args ) { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $assoc_args = apply_filters( 'woocommerce_cli_create_tax_rate_data', $assoc_args ); $tax_data = array( 'tax_rate_country' => '', 'tax_rate_state' => '', 'tax_rate' => '', 'tax_rate_name' => '', 'tax_rate_priority' => 1, 'tax_rate_compound' => 0, 'tax_rate_shipping' => 1, 'tax_rate_order' => 0, 'tax_rate_class' => '', ); foreach ( $tax_data as $key => $value ) { $new_key = str_replace( 'tax_rate_', '', $key ); $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; if ( isset( $assoc_args[ $new_key ] ) ) { if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { $tax_data[ $key ] = $assoc_args[ $new_key ] ? 1 : 0; } else { $tax_data[ $key ] = $assoc_args[ $new_key ]; } } } // Create tax rate. $id = WC_Tax::_insert_tax_rate( $tax_data ); // Add locales. if ( ! empty( $assoc_args['postcode'] ) ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $assoc_args['postcode'] ) ); } if ( ! empty( $assoc_args['city'] ) ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $assoc_args['city'] ) ); } do_action( 'woocommerce_cli_create_tax_rate', $id, $tax_data ); if ( $porcelain ) { WP_CLI::line( $id ); } else { WP_CLI::success( "Created tax rate $id." ); } } /** * Create a tax class. * * ## OPTIONS * * [--=] * : Associative args for the new tax class. * * [--porcelain] * : Outputs just the new tax class slug. * * ## AVAILABLE FIELDS * * These fields are available for create command: * * * name * * ## EXAMPLES * * wp wc tax create_class --name="Reduced Rate" * * @since 2.5.0 */ public function create_class( $__, $assoc_args ) { try { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $assoc_args = apply_filters( 'woocommerce_cli_create_tax_class_data', $assoc_args ); // Check if name is specified. if ( ! isset( $assoc_args['name'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_missing_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ) ); } $name = sanitize_text_field( $assoc_args['name'] ); $slug = sanitize_title( $name ); $classes = WC_Tax::get_tax_classes(); $exists = false; // Check if class exists. foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $slug ) { $exists = true; break; } } // Return error if tax class already exists. if ( $exists ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_create_tax_class', __( 'Tax class already exists', 'woocommerce' ) ); } // Add the new class $classes[] = $name; update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); do_action( 'woocommerce_cli_create_tax_class', $slug, array( 'name' => $name ) ); if ( $porcelain ) { WP_CLI::line( $slug ); } else { WP_CLI::success( "Created tax class $slug." ); } } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Delete one or more tax rates. * * ## OPTIONS * * ... * : The tax rate ID to delete. * * ## EXAMPLES * * wp wc tax delete 123 * * wp wc tax delete $(wp wc tax list --format=ids) * * @since 2.5.0 */ public function delete( $args, $assoc_args ) { $exit_code = 0; foreach ( $args as $tax_id ) { $tax_id = absint( $tax_id ); $tax = WC_Tax::_get_tax_rate( $tax_id ); if ( is_null( $tax ) ) { $exit_code += 1; WP_CLI::warning( "Failed deleting tax rate {$tax_id}." ); continue; } do_action( 'woocommerce_cli_delete_tax_rate', $tax_id ); WC_Tax::_delete_tax_rate( $tax_id ); WP_CLI::success( "Deleted tax rate {$tax_id}." ); } exit( $exit_code ? 1 : 0 ); } /** * Delete one or more tax classes. * * ## OPTIONS * * ... * : The tax class slug to delete. * * ## EXAMPLES * * wp wc tax delete_class reduced-rate * * wp wc tax delete_class $(wp wc tax list_class --format=ids) * * @since 2.5.0 */ public function delete_class( $args, $assoc_args ) { $classes = WC_Tax::get_tax_classes(); $exit_code = 0; foreach ( $args as $slug ) { $slug = sanitize_title( $slug ); $deleted = false; foreach ( $classes as $key => $class ) { if ( sanitize_title( $class ) === $slug ) { unset( $classes[ $key ] ); $deleted = true; break; } } if ( $deleted ) { WP_CLI::success( "Deleted tax class {$slug}." ); } else { $exit_code += 1; WP_CLI::warning( "Failed deleting tax class {$slug}." ); } } update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); exit( $exit_code ? 1 : 0 ); } /** * Get a tax rate. * * ## OPTIONS * * * : Tax rate ID * * [--field=] * : Instead of returning the whole tax rate fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the tax rates fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * These fields are available for get command: * * * id * * country * * state * * postcode * * city * * rate * * name * * priority * * compound * * shipping * * order * * class * * ## EXAMPLES * * wp wc tax get 123 --field=rate * * wp wc tax get 321 --format=json > rate321.json * * @since 2.5.0 */ public function get( $args, $assoc_args ) { global $wpdb; try { $tax_data = $this->format_taxes_to_items( array( $args[0] ) ); if ( empty( $tax_data ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_tax_rate', sprintf( __( 'Invalid tax rate ID: %s', 'woocommerce' ), $args[0] ) ); } $tax_data = apply_filters( 'woocommerce_cli_get_tax_rate', $tax_data[0] ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $tax_data ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $tax_data ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List taxes. * * ## OPTIONS * * [--=] * : Filter tax based on tax property. * * [--field=] * : Prints the value of a single field for each tax. * * [--fields=] * : Limit the output to specific tax fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each tax: * * * id * * country * * state * * postcode * * city * * rate * * name * * priority * * compound * * shipping * * class * * These fields are optionally available: * * * order * * Fields for filtering query result also available: * * * class Sort by tax class. * * page Page number. * * ## EXAMPLES * * wp wc tax list * * wp wc tax list --field=id * * wp wc tax list --fields=id,rate,class --format=json * * @since 2.5.0 * @subcommand list */ public function list_( $__, $assoc_args ) { $query_args = $this->merge_tax_query_args( array(), $assoc_args ); $formatter = $this->get_formatter( $assoc_args ); $taxes = $this->query_tax_rates( $query_args ); if ( 'ids' === $formatter->format ) { $_taxes = array(); foreach ( $taxes as $tax ) { $_taxes[] = $tax->tax_rate_id; } echo implode( ' ', $_taxes ); } else { $items = $this->format_taxes_to_items( $taxes ); $formatter->display_items( $items ); } } /** * List tax classes. * * ## OPTIONS * * [--=] * : Filter tax class based on tax class property. * * [--field=] * : Prints the value of a single field for each tax class. * * [--fields=] * : Limit the output to specific tax class fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each tax class: * * * slug * * name * * ## EXAMPLES * * wp wc tax list_class * * wp wc tax list_class --field=slug * * wp wc tax list_class --format=json * * @since 2.5.0 * @subcommand list_class */ public function list_class( $__, $assoc_args ) { // Set default fields for tax classes if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = 'slug,name'; } $formatter = $this->get_formatter( $assoc_args ); $items = array(); // Add standard class $items[] = array( 'slug' => 'standard', 'name' => __( 'Standard Rate', 'woocommerce' ) ); $classes = WC_Tax::get_tax_classes(); foreach ( $classes as $class ) { $items[] = apply_filters( 'woocommerce_cli_tax_class_response', array( 'slug' => sanitize_title( $class ), 'name' => $class ), $class, $assoc_args, $this ); } if ( 'ids' === $formatter->format ) { $_slugs = array(); foreach ( $items as $item ) { $_slugs[] = $item['slug']; } echo implode( ' ', $_slugs ); } else { $formatter->display_items( $items ); } } /** * Update a tax rate. * * ## OPTIONS * * * : The ID of the tax rate to update. * * [--=] * : One or more fields to update. * * ## AVAILABLE FIELDS * * These fields are available for update command: * * * country * * state * * postcode * * city * * rate * * name * * priority * * compound * * shipping * * class * * ## EXAMPLES * * wp wc tax update 123 --rate=5 * * @since 2.5.0 */ public function update( $args, $assoc_args ) { try { // Get current tax rate data $tax_data = $this->format_taxes_to_items( array( $args[0] ) ); if ( empty( $tax_data ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_tax_rate', sprintf( __( 'Invalid tax rate ID: %s', 'woocommerce' ), $args[0] ) ); } $current_data = $tax_data[0]; $id = $current_data['id']; $data = apply_filters( 'woocommerce_cli_update_tax_rate_data', $assoc_args, $id ); $new_data = array(); $default_fields = array( 'tax_rate_country', 'tax_rate_state', 'tax_rate', 'tax_rate_name', 'tax_rate_priority', 'tax_rate_compound', 'tax_rate_shipping', 'tax_rate_order', 'tax_rate_class' ); foreach ( $data as $key => $value ) { $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; // Check if the key is valid if ( ! in_array( $new_key, $default_fields ) ) { continue; } // Test new data against current data if ( $value === $current_data[ $key ] ) { continue; } // Fix compund and shipping values if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { $value = $value ? 1 : 0; } $new_data[ $new_key ] = $value; } // Update tax rate WC_Tax::_update_tax_rate( $id, $new_data ); // Update locales if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); } if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); } do_action( 'woocommerce_cli_update_tax_rate', $id, $data ); WP_CLI::success( "Updated tax rate $id." ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Add common cli arguments to argument list before $wpdb->get_results() is run. * * @since 2.5.0 * @param array $base_args Required arguments for the query (e.g. `limit`, etc) * @param array $assoc_args Arguments provided in when invoking the command * @return array */ protected function merge_tax_query_args( $base_args, $assoc_args ) { $args = array(); if ( ! empty( $assoc_args['class'] ) ) { $args['class'] = $assoc_args['class']; } // Number of post to show per page. if ( ! empty( $assoc_args['limit'] ) ) { $args['posts_per_page'] = $assoc_args['limit']; } // posts page. $args['paged'] = ( isset( $assoc_args['page'] ) ) ? absint( $assoc_args['page'] ) : 1; $args = apply_filters( 'woocommerce_cli_tax_query_args', $args, $assoc_args ); return array_merge( $base_args, $args ); } /** * Helper method to get tax rates objects * * @since 2.5.0 * * @param array $args * * @return array */ protected function query_tax_rates( $args ) { global $wpdb; $query = " SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates WHERE 1 = 1 "; // Filter by tax class if ( ! empty( $args['class'] ) ) { $class = 'standard' !== $args['class'] ? sanitize_title( $args['class'] ) : ''; $query .= " AND tax_rate_class = '$class'"; } // Order tax rates $order_by = ' ORDER BY tax_rate_order'; // Pagination $per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ); $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); return $wpdb->get_results( $query . $order_by . $pagination ); } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,country,state,postcode,city,rate,name,priority,compound,shipping,class'; } /** * Format taxes from query result to items in which each item contain * common properties of item, for instance `tax_rate_id` will be `id`. * * @since 2.5.0 * @param array $taxes Array of tax rate. * @return array Items */ protected function format_taxes_to_items( $taxes ) { global $wpdb; $items = array(); foreach ( $taxes as $tax_id ) { $id = is_object( $tax_id ) ? $tax_id->tax_rate_id : $tax_id; $id = absint( $id ); // Get tax rate details $tax = WC_Tax::_get_tax_rate( $id ); if ( is_wp_error( $tax ) || empty( $tax ) ) { continue; } $tax_data = array( 'id' => $tax['tax_rate_id'], 'country' => $tax['tax_rate_country'], 'state' => $tax['tax_rate_state'], 'postcode' => '', 'city' => '', 'rate' => $tax['tax_rate'], 'name' => $tax['tax_rate_name'], 'priority' => (int) $tax['tax_rate_priority'], 'compound' => (bool) $tax['tax_rate_compound'], 'shipping' => (bool) $tax['tax_rate_shipping'], 'order' => (int) $tax['tax_rate_order'], 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard' ); // Get locales from a tax rate $locales = $wpdb->get_results( $wpdb->prepare( " SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d ", $id ) ); if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { foreach ( $locales as $locale ) { $tax_data[ $locale->location_type ] = $locale->location_code; } } $items[] = $tax_data; } return $items; } } class-wc-cli-order.php000066600000112135152140537160010662 0ustar00=] * : Associative args for the new order. * * [--porcelain] * : Outputs just the new order id. * * ## AVAILABLE FIELDS * * Required fields: * * * customer_id * * Optional fields: * * * status * * note * * currency * * order_meta * * Payment detail fields: * * * payment_details.method_id * * payment_details.method_title * * payment_details.paid * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * Line item fields (numeric array, started with index zero): * * * line_items.0.product_id * * line_items.0.quantity * * line_items.0.variations.pa_color * * For second line item: line_items.1.product_id and so on. * * Shipping line fields (numeric array, started with index zero): * * * shipping_lines.0.method_id * * shipping_lines.0.method_title * * shipping_lines.0.total * * For second shipping item: shipping_lines.1.method_id and so on. * * ## EXAMPLES * * wp wc order create --customer_id=1 --status=pending ... * * @since 2.5.0 */ public function create( $__, $assoc_args ) { global $wpdb; wc_transaction_query( 'start' ); try { $porcelain = isset( $assoc_args['porcelain'] ); unset( $assoc_args['porcelain'] ); $data = apply_filters( 'woocommerce_cli_create_order_data', $this->unflatten_array( $assoc_args ) ); // default order args, note that status is checked for validity in wc_create_order() $default_order_args = array( 'status' => isset( $data['status'] ) ? $data['status'] : '', 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, ); if ( empty( $data['customer_id'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_missing_customer_id', __( 'Missing customer_id field', 'woocommerce' ) ); } // make sure customer exists if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_customer_id', __( 'Customer ID is invalid', 'woocommerce' ) ); } $default_order_args['customer_id'] = $data['customer_id']; // create the pending order $order = $this->create_base_order( $default_order_args, $data ); if ( is_wp_error( $order ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ) ); } // billing/shipping addresses $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $set_item = "set_{$line_type}"; foreach ( $data[ $line ] as $item ) { $this->$set_item( $order, $item, 'create' ); } } } // calculate totals and set them $order->calculate_totals(); // payment method (and payment_complete() if `paid` == true) if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // method ID & title are required if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ) ); } update_post_meta( $order->id, '_payment_method', $data['payment_details']['method_id'] ); update_post_meta( $order->id, '_payment_method_title', $data['payment_details']['method_title'] ); // Mark as paid if set. if ( isset( $data['payment_details']['paid'] ) && $this->is_true( $data['payment_details']['paid'] ) ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // Set order currency. if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid', 'woocommerce') ); } update_post_meta( $order->id, '_order_currency', $data['currency'] ); } // Set order meta. if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->id, $data['order_meta'] ); } wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_cli_create_order', $order->id, $data ); wc_transaction_query( 'commit' ); if ( $porcelain ) { WP_CLI::line( $order->id ); } else { WP_CLI::success( "Created order {$order->id}." ); } } catch ( WC_CLI_Exception $e ) { wc_transaction_query( 'rollback' ); WP_CLI::error( $e->getMessage() ); } } /** * Delete one or more orders. * * ## OPTIONS * * ... * : The order ID to delete. * * ## EXAMPLES * * wp wc order delete 123 * * @since 2.5.0 */ public function delete( $args, $assoc_args ) { $exit_code = 0; foreach ( $args as $id ) { $order = wc_get_order( $id ); if ( ! $order ) { WP_CLI::warning( "Invalid order ID $id" ); continue; } wc_delete_shop_order_transients( $id ); do_action( 'woocommerce_cli_delete_order', $id ); $r = wp_delete_post( $id, true ); if ( $r ) { WP_CLI::success( "Deleted order $id." ); } else { $exit_code += 1; WP_CLI::warning( "Failed deleting order $id." ); } } exit( $exit_code ? 1 : 0 ); } /** * Get an order. * * ## OPTIONS * * * : Order ID. * * [--field=] * : Instead of returning the whole order fields, returns the value of a single fields. * * [--fields=] * : Get a specific subset of the order's fields. * * [--format=] * : Accepted values: table, json, csv. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default: * * * id * * order_number * * customer_id * * total * * status * * created_at * * For more fields, see: wp wc order list --help * * ## EXAMPLES * * wp wc order get 123 --fields=id,title,sku * * @since 2.5.0 */ public function get( $args, $assoc_args ) { try { $order = wc_get_order( $args[0] ); if ( ! $order ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_order', sprintf( __( 'Invalid order "%s"', 'woocommerce' ), $args[0] ) ); } $order_data = $this->get_order_data( $order ); $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $order_data ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * List orders. * * ## OPTIONS * * [--=] * : Filter orders based on order property. * * [--field=] * : Prints the value of a single field for each order. * * [--fields=] * : Limit the output to specific order fields. * * [--format=] * : Acceptec values: table, csv, json, count, ids. Default: table. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each order: * * * id * * order_number * * customer_id * * total * * status * * created_at * * These fields are optionally available: * * * updated_at * * completed_at * * currency * * subtotal * * total_line_items_quantity * * total_tax * * total_shipping * * cart_tax * * shipping_tax * * total_discount * * shipping_methods * * note * * customer_ip * * customer_user_agent * * view_order_url * * Payment detail fields: * * * payment_details.method_id * * payment_details.method_title * * payment_details.paid * * Billing address fields: * * * billing_address.first_name * * billing_address.last_name * * billing_address.company * * billing_address.address_1 * * billing_address.address_2 * * billing_address.city * * billing_address.state * * billing_address.postcode * * billing_address.country * * billing_address.email * * billing_address.phone * * Shipping address fields: * * * shipping_address.first_name * * shipping_address.last_name * * shipping_address.company * * shipping_address.address_1 * * shipping_address.address_2 * * shipping_address.city * * shipping_address.state * * shipping_address.postcode * * shipping_address.country * * Line item fields (numeric array, started with index zero): * * * line_items.0.product_id * * line_items.0.quantity * * line_items.0.variations.pa_color * * For second line item: line_items.1.product_id and so on. * * Shipping line fields (numeric array, started with index zero): * * * shipping_lines.0.method_id * * shipping_lines.0.method_title * * shipping_lines.0.total * * For second shipping item: shipping_lines.1.method_id and so on. * * ## EXAMPLES * * wp wc order list * * @subcommand list * @since 2.5.0 */ public function list_( $args, $assoc_args ) { $query_args = $this->merge_wp_query_args( $this->get_list_query_args( $assoc_args ), $assoc_args ); $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_Query( $query_args ); echo implode( ' ', $query->posts ); } else { $query = new WP_Query( $query_args ); $items = $this->format_posts_to_items( $query->posts ); $formatter->display_items( $items ); } } /** * Update an order. * * ## OPTIONS * * * : Product ID * * [--=] * : One or more fields to update. * * ## AVAILABLE FIELDS * * For available fields, see: wp wc order create --help * * ## EXAMPLES * * wp wc order update 123 --status=completed * * @todo gedex * @since 2.5.0 */ public function update( $args, $assoc_args ) { try { $id = $args[0]; $data = apply_filters( 'woocommerce_cli_update_order_data', $this->unflatten_array( $assoc_args ) ); $update_totals = false; $order = wc_get_order( $id ); if ( empty( $order ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ) ); } $order_args = array( 'order_id' => $order->id ); // customer note if ( isset( $data['note'] ) ) { $order_args['customer_note'] = $data['note']; } // order status if ( ! empty( $data['status'] ) ) { $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); } // customer ID if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { // make sure customer exists if ( false === get_user_by( 'id', $data['customer_id'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_customer_id', __( 'Customer ID is invalid', 'woocommerce' ) ); } update_post_meta( $order->id, '_customer_user', $data['customer_id'] ); } // billing/shipping address $this->set_order_addresses( $order, $data ); $lines = array( 'line_item' => 'line_items', 'shipping' => 'shipping_lines', 'fee' => 'fee_lines', 'coupon' => 'coupon_lines', ); foreach ( $lines as $line_type => $line ) { if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { $update_totals = true; foreach ( $data[ $line ] as $item ) { // item ID is always required if ( ! array_key_exists( 'id', $item ) ) { $item['id'] = null; } // create item if ( is_null( $item['id'] ) ) { $this->set_item( $order, $line_type, $item, 'create' ); } elseif ( $this->item_is_null( $item ) ) { // delete item wc_delete_order_item( $item['id'] ); } else { // update item $this->set_item( $order, $line_type, $item, 'update' ); } } } } // payment method (and payment_complete() if `paid` == true and order needs payment) if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { // method ID if ( isset( $data['payment_details']['method_id'] ) ) { update_post_meta( $order->id, '_payment_method', $data['payment_details']['method_id'] ); } // method title if ( isset( $data['payment_details']['method_title'] ) ) { update_post_meta( $order->id, '_payment_method_title', $data['payment_details']['method_title'] ); } // mark as paid if set if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && $this->is_true( $data['payment_details']['paid'] ) ) { $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); } } // set order currency if ( isset( $data['currency'] ) ) { if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid', 'woocommerce' ) ); } update_post_meta( $order->id, '_order_currency', $data['currency'] ); } // set order number if ( isset( $data['order_number'] ) ) { update_post_meta( $order->id, '_order_number', $data['order_number'] ); } // if items have changed, recalculate order totals if ( $update_totals ) { $order->calculate_totals(); } // update order meta if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { $this->set_order_meta( $order->id, $data['order_meta'] ); } // update the order post to set customer note/modified date wc_update_order( $order_args ); wc_delete_shop_order_transients( $order->id ); do_action( 'woocommerce_cli_update_order', $order->id, $data ); WP_CLI::success( "Updated order {$order->id}." ); } catch ( WC_CLI_Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Get query args for list subcommand. * * @since 2.5.0 * @param array $args Args from command line * @return array */ protected function get_list_query_args( $args ) { $query_args = array( 'post_type' => 'shop_order', 'post_status' => array_keys( wc_get_order_statuses() ), 'posts_per_page' => -1, ); if ( ! empty( $args['status'] ) ) { $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); $statuses = explode( ',', $statuses ); $query_args['post_status'] = $statuses; } if ( ! empty( $args['customer_id'] ) ) { $query_args['meta_query'] = array( array( 'key' => '_customer_user', 'value' => (int) $args['customer_id'], 'compare' => '=' ) ); } return $query_args; } /** * Get default format fields that will be used in `list` and `get` subcommands. * * @since 2.5.0 * @return string */ protected function get_default_format_fields() { return 'id,order_number,customer_id,total,status,created_at'; } /** * Format posts from WP_Query result to items in which each item contain * common properties of item. * * @since 2.5.0 * @param array $posts Array of post * @return array Items */ protected function format_posts_to_items( $posts ) { $items = array(); foreach ( $posts as $post ) { $order = wc_get_order( $post->ID ); if ( ! $order ) { continue; } $items[] = $this->get_order_data( $order ); } return $items; } /** * Get the order data for the given ID. * * @since 2.5.0 * @param WC_Order $order The order instance * @return array */ protected function get_order_data( $order ) { $order_post = get_post( $order->id ); $dp = wc_get_price_decimals(); $order_data = array( 'id' => $order->id, 'order_number' => $order->get_order_number(), 'created_at' => $this->format_datetime( $order_post->post_date_gmt ), 'updated_at' => $this->format_datetime( $order_post->post_modified_gmt ), 'completed_at' => $this->format_datetime( $order->completed_date, true ), 'status' => $order->get_status(), 'currency' => $order->get_order_currency(), 'total' => wc_format_decimal( $order->get_total(), $dp ), 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), 'total_line_items_quantity' => $order->get_item_count(), 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), 'total_shipping' => wc_format_decimal( $order->get_total_shipping(), $dp ), 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), 'shipping_methods' => $order->get_shipping_method(), 'payment_details' => array( 'method_id' => $order->payment_method, 'method_title' => $order->payment_method_title, 'paid' => isset( $order->paid_date ), ), 'billing_address' => array( 'first_name' => $order->billing_first_name, 'last_name' => $order->billing_last_name, 'company' => $order->billing_company, 'address_1' => $order->billing_address_1, 'address_2' => $order->billing_address_2, 'city' => $order->billing_city, 'state' => $order->billing_state, 'postcode' => $order->billing_postcode, 'country' => $order->billing_country, 'email' => $order->billing_email, 'phone' => $order->billing_phone, ), 'shipping_address' => array( 'first_name' => $order->shipping_first_name, 'last_name' => $order->shipping_last_name, 'company' => $order->shipping_company, 'address_1' => $order->shipping_address_1, 'address_2' => $order->shipping_address_2, 'city' => $order->shipping_city, 'state' => $order->shipping_state, 'postcode' => $order->shipping_postcode, 'country' => $order->shipping_country, ), 'note' => $order->customer_note, 'customer_ip' => $order->customer_ip_address, 'customer_user_agent' => $order->customer_user_agent, 'customer_id' => $order->get_user_id(), 'view_order_url' => $order->get_view_order_url(), 'line_items' => array(), 'shipping_lines' => array(), 'tax_lines' => array(), 'fee_lines' => array(), 'coupon_lines' => array(), ); // add line items foreach ( $order->get_items() as $item_id => $item ) { $product = $order->get_product_from_item( $item ); $product_id = null; $product_sku = null; // Check if the product exists. if ( is_object( $product ) ) { $product_id = ( isset( $product->variation_id ) ) ? $product->variation_id : $product->id; $product_sku = $product->get_sku(); } $meta = new WC_Order_Item_Meta( $item, $product ); $item_meta = array(); foreach ( $meta->get_formatted( null ) as $meta_key => $formatted_meta ) { $item_meta[] = array( 'key' => $meta_key, 'label' => $formatted_meta['label'], 'value' => $formatted_meta['value'], ); } $order_data['line_items'][] = array( 'id' => $item_id, 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), 'quantity' => wc_stock_amount( $item['qty'] ), 'tax_class' => ( ! empty( $item['tax_class'] ) ) ? $item['tax_class'] : null, 'name' => $item['name'], 'product_id' => $product_id, 'sku' => $product_sku, 'meta' => $item_meta, ); } // Add shipping. foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { $order_data['shipping_lines'][] = array( 'id' => $shipping_item_id, 'method_id' => $shipping_item['method_id'], 'method_title' => $shipping_item['name'], 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), ); } // Add taxes. foreach ( $order->get_tax_totals() as $tax_code => $tax ) { $order_data['tax_lines'][] = array( 'id' => $tax->id, 'rate_id' => $tax->rate_id, 'code' => $tax_code, 'title' => $tax->label, 'total' => wc_format_decimal( $tax->amount, $dp ), 'compound' => (bool) $tax->is_compound, ); } // Add fees. foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { $order_data['fee_lines'][] = array( 'id' => $fee_item_id, 'title' => $fee_item['name'], 'tax_class' => ( ! empty( $fee_item['tax_class'] ) ) ? $fee_item['tax_class'] : null, 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), ); } // Add coupons. foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { $order_data['coupon_lines'][] = array( 'id' => $coupon_item_id, 'code' => $coupon_item['name'], 'amount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), ); } $order_data = apply_filters( 'woocommerce_cli_order_data', $order_data ); return $this->flatten_array( $order_data ); } /** * Creates new WC_Order. * * @since 2.5.0 * @param $args array * @return WC_Order */ protected function create_base_order( $args ) { return wc_create_order( $args ); } /** * Helper method to set/update the billing & shipping addresses for an order. * * @since 2.5.0 * @param WC_Order $order * @param array $data */ protected function set_order_addresses( $order, $data ) { $address_fields = array( 'first_name', 'last_name', 'company', 'email', 'phone', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country', ); $billing_address = $shipping_address = array(); // billing address. if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['billing_address'][ $field ] ) ) { $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); } } unset( $address_fields['email'] ); unset( $address_fields['phone'] ); } // shipping address. if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { foreach ( $address_fields as $field ) { if ( isset( $data['shipping_address'][ $field ] ) ) { $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); } } } $order->set_address( $billing_address, 'billing' ); $order->set_address( $shipping_address, 'shipping' ); // update user meta if ( $order->get_user_id() ) { foreach ( $billing_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); } foreach ( $shipping_address as $key => $value ) { update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); } } } /** * Helper method to add/update order meta, with two restrictions: * * 1) Only non-protected meta (no leading underscore) can be set * 2) Meta values must be scalar (int, string, bool) * * @since 2.5.0 * @param int $order_id valid order ID * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format */ protected function set_order_meta( $order_id, $order_meta ) { foreach ( $order_meta as $meta_key => $meta_value ) { if ( is_string( $meta_key) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { update_post_meta( $order_id, $meta_key, $meta_value ); } } } /** * Wrapper method to create/update order items * * When updating, the item ID provided is checked to ensure it is associated * with the order. * * @since 2.5.0 * @param WC_Order $order order * @param string $item_type * @param array $item item provided in the request body * @param string $action either 'create' or 'update' * @throws WC_CLI_Exception if item ID is not associated with order */ protected function set_item( $order, $item_type, $item, $action ) { global $wpdb; $set_method = "set_{$item_type}"; // verify provided line item ID is associated with order if ( 'update' === $action ) { $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", absint( $item['id'] ), absint( $order->id ) ) ); if ( is_null( $result ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order', 'woocommerce' ) ); } } $this->$set_method( $order, $item, $action ); } /** * Create or update a line item * * @since 2.5.0 * @param WC_Order $order * @param array $item line item data * @param string $action 'create' to add line item or 'update' to update it * @throws WC_CLI_Exception invalid data, server error */ protected function set_line_item( $order, $item, $action ) { $creating = ( 'create' === $action ); // product is always required if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ) ); } // when updating, ensure product ID provided matches if ( 'update' === $action ) { $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ) ); } } if ( isset( $item['product_id'] ) ) { $product_id = $item['product_id']; } elseif ( isset( $item['sku'] ) ) { $product_id = wc_get_product_id_by_sku( $item['sku'] ); } // variations must each have a key & value $variation_id = 0; if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { foreach ( $item['variations'] as $key => $value ) { if ( ! $key || ! $value ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ) ); } } $item_args['variation'] = $item['variations']; $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item_args['variation'] ); } $product = wc_get_product( $variation_id ? $variation_id : $product_id ); // must be a valid WC_Product if ( ! is_object( $product ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product', __( 'Product is invalid', 'woocommerce' ) ); } // quantity must be positive float if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_quantity', __( 'Product quantity must be a positive float', 'woocommerce' ) ); } // quantity is required when creating if ( $creating && ! isset( $item['quantity'] ) ) { throw new WC_CLI_Exception( 'woocommerce_cli_invalid_product_quantity', __( 'Product quantity is required', 'woocommerce' ) ); } $item_args = array(); // quantity if ( isset( $item['quantity'] ) ) { $item_args['qty'] = $item['quantity']; } // total if ( isset( $item['total'] ) ) { $item_args['totals']['total'] = floatval( $item['total'] ); } // total tax if ( isset( $item['total_tax'] ) ) { $item_args['totals']['tax'] = floatval( $item['total_tax'] ); } // subtotal if ( isset( $item['subtotal'] ) ) { $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); } // subtotal tax if ( isset( $item['subtotal_tax'] ) ) { $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); } $item_args = apply_filters( 'woocommerce_cli_order_line_item_args', $item_args, $item, $order, $action ); if ( $creating ) { $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); if ( ! $item_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again', 'woocommerce' ) ); } } else { $item_id = $order->update_product( $item['id'], $product, $item_args ); if ( ! $item_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_update_line_item', __( 'Cannot update line item, try again', 'woocommerce' ) ); } } } /** * Given a product ID & variations, find the correct variation ID to use for * calculation. We can't just trust input from the CLI to pass a variation_id * manually, otherwise you could pass the cheapest variation ID but provide * other information so we have to look up the variation ID. * * @since 2.5.0 * @param WC_Product $product Product instance * @return int Returns an ID if a valid variation was found for this product */ protected function get_variation_id( $product, $variations = array() ) { $variation_id = null; $variations_normalized = array(); if ( $product->is_type( 'variable' ) && $product->has_child() ) { if ( isset( $variations ) && is_array( $variations ) ) { // start by normalizing the passed variations foreach ( $variations as $key => $value ) { $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); // from get_attributes in class-wc-api-products.php $variations_normalized[ $key ] = strtolower( $value ); } // now search through each product child and see if our passed variations match anything foreach ( $product->get_children() as $variation ) { $meta = array(); foreach ( get_post_meta( $variation ) as $key => $value ) { $value = $value[0]; $key = str_replace( 'attribute_', '', str_replace( 'pa_', '', $key ) ); $meta[ $key ] = strtolower( $value ); } // if the variation array is a part of the $meta array, we found our match if ( $this->array_contains( $variations_normalized, $meta ) ) { $variation_id = $variation; break; } } } } return $variation_id; } /** * Utility function to see if the meta array contains data from variations. * * @since 2.5.0 * @return bool Returns true if meta array contains data from variations */ protected function array_contains( $needles, $haystack ) { foreach ( $needles as $key => $value ) { if ( $haystack[ $key ] !== $value ) { return false; } } return true; } /** * Create or update an order shipping method * * @since 2.5.0 * @param \WC_Order $order * @param array $shipping item data * @param string $action 'create' to add shipping or 'update' to update it * @throws WC_CLI_Exception invalid data, server error */ protected function set_shipping( $order, $shipping, $action ) { // total must be a positive float if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { throw new WC_CLI_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount', 'woocommerce' ) ); } if ( 'create' === $action ) { // method ID is required if ( ! isset( $shipping['method_id'] ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required', 'woocommerce' ) ); } $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); $shipping_id = $order->add_shipping( $rate ); if ( ! $shipping_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_create_shipping', __( 'Cannot create shipping method, try again', 'woocommerce' ) ); } } else { $shipping_args = array(); if ( isset( $shipping['method_id'] ) ) { $shipping_args['method_id'] = $shipping['method_id']; } if ( isset( $shipping['method_title'] ) ) { $shipping_args['method_title'] = $shipping['method_title']; } if ( isset( $shipping['total'] ) ) { $shipping_args['cost'] = floatval( $shipping['total'] ); } $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); if ( ! $shipping_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again', 'woocommerce' ) ); } } } /** * Create or update an order fee. * * @since 2.5.0 * @param \WC_Order $order * @param array $fee item data * @param string $action 'create' to add fee or 'update' to update it * @throws WC_CLI_Exception invalid data, server error */ protected function set_fee( $order, $fee, $action ) { if ( 'create' === $action ) { // fee title is required if ( ! isset( $fee['title'] ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ) ); } $order_fee = new stdClass(); $order_fee->id = sanitize_title( $fee['title'] ); $order_fee->name = $fee['title']; $order_fee->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; $order_fee->taxable = false; $order_fee->tax = 0; $order_fee->tax_data = array(); $order_fee->tax_class = ''; // if taxable, tax class and total are required if ( isset( $fee['taxable'] ) && $fee['taxable'] ) { if ( ! isset( $fee['tax_class'] ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable', 'woocommerce' ) ); } $order_fee->taxable = true; $order_fee->tax_class = $fee['tax_class']; if ( isset( $fee['total_tax'] ) ) { $order_fee->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; } if ( isset( $fee['tax_data'] ) ) { $order_fee->tax = wc_format_refund_total( array_sum( $fee['tax_data'] ) ); $order_fee->tax_data = array_map( 'wc_format_refund_total', $fee['tax_data'] ); } } $fee_id = $order->add_fee( $order_fee ); if ( ! $fee_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_create_fee', __( 'Cannot create fee, try again', 'woocommerce' ) ); } } else { $fee_args = array(); if ( isset( $fee['title'] ) ) { $fee_args['name'] = $fee['title']; } if ( isset( $fee['tax_class'] ) ) { $fee_args['tax_class'] = $fee['tax_class']; } if ( isset( $fee['total'] ) ) { $fee_args['line_total'] = floatval( $fee['total'] ); } if ( isset( $fee['total_tax'] ) ) { $fee_args['line_tax'] = floatval( $fee['total_tax'] ); } $fee_id = $order->update_fee( $fee['id'], $fee_args ); if ( ! $fee_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again', 'woocommerce' ) ); } } } /** * Create or update an order coupon * * @since 2.5.0 * @param \WC_Order $order * @param array $coupon item data * @param string $action 'create' to add coupon or 'update' to update it * @throws WC_CLI_Exception invalid data, server error */ protected function set_coupon( $order, $coupon, $action ) { // coupon amount must be positive float. if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { throw new WC_CLI_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount', 'woocommerce' ) ); } if ( 'create' === $action ) { // coupon code is required if ( empty( $coupon['code'] ) ) { throw new WC_CLI_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required', 'woocommerce' ) ); } $coupon_id = $order->add_coupon( $coupon['code'], isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0 ); if ( ! $coupon_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_create_order_coupon', __( 'Cannot create coupon, try again', 'woocommerce' ) ); } } else { $coupon_args = array(); if ( isset( $coupon['code'] ) ) { $coupon_args['code'] = $coupon['code']; } if ( isset( $coupon['amount'] ) ) { $coupon_args['discount_amount'] = floatval( $coupon['amount'] ); } $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); if ( ! $coupon_id ) { throw new WC_CLI_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again', 'woocommerce' ) ); } } } }