__( 'Increase by %', 'gridsmith' ), 'decrease_percent' => __( 'Decrease by %', 'gridsmith' ), 'increase_amount' => __( 'Increase by fixed amount', 'gridsmith' ), 'decrease_amount' => __( 'Decrease by fixed amount', 'gridsmith' ), 'set' => __( 'Set to', 'gridsmith' ), 'regular_minus_percent' => __( 'Regular price minus % (sale price only)', 'gridsmith' ), 'clear' => __( 'Remove (sale price only)', 'gridsmith' ), ); } /** * Registered bulk actions. * * @return array name => array( label, params, compute ) */ public static function actions() { $actions = array( 'price' => array( 'label' => __( 'Bulk price change', 'gridsmith' ), 'params' => array( __CLASS__, 'price_params' ), 'compute' => array( __CLASS__, 'price_compute' ), 'prices' => true, ), ); return (array) apply_filters( 'bdbe_bulk_actions', $actions ); } /** * Sanitise price parameters. * * @param array $raw Raw params. * @return array|WP_Error */ public static function price_params( array $raw ) { $target = isset( $raw['target'] ) ? sanitize_key( $raw['target'] ) : ''; $mode = isset( $raw['mode'] ) ? sanitize_key( $raw['mode'] ) : ''; $value = isset( $raw['value'] ) ? wc_format_decimal( sanitize_text_field( (string) $raw['value'] ) ) : ''; if ( ! in_array( $target, array( 'regular_price', 'sale_price' ), true ) ) { return new WP_Error( 'bdbe_target', __( 'Choose which price to change.', 'gridsmith' ), array( 'status' => 400 ) ); } if ( ! array_key_exists( $mode, self::price_modes() ) ) { return new WP_Error( 'bdbe_mode', __( 'Choose how to change the price.', 'gridsmith' ), array( 'status' => 400 ) ); } if ( in_array( $mode, array( 'regular_minus_percent', 'clear' ), true ) && 'sale_price' !== $target ) { return new WP_Error( 'bdbe_mode', __( 'This option only works on the sale price.', 'gridsmith' ), array( 'status' => 400 ) ); } if ( 'clear' !== $mode && ( '' === $value || ! is_numeric( $value ) || (float) $value < 0 ) ) { return new WP_Error( 'bdbe_value', __( 'Enter a number of 0 or more.', 'gridsmith' ), array( 'status' => 400 ) ); } if ( in_array( $mode, array( 'decrease_percent', 'regular_minus_percent' ), true ) && (float) $value > 100 ) { return new WP_Error( 'bdbe_value', __( 'A percentage decrease cannot be more than 100.', 'gridsmith' ), array( 'status' => 400 ) ); } $labels = array( 'regular_price' => __( 'Regular price', 'gridsmith' ), 'sale_price' => __( 'Sale price', 'gridsmith' ), ); $signs = array( 'increase_percent' => '+' . $value . '%', 'decrease_percent' => '−' . $value . '%', 'increase_amount' => '+' . $value, 'decrease_amount' => '−' . $value, 'set' => '= ' . $value, /* translators: %s: percentage. */ 'regular_minus_percent' => sprintf( __( '= regular − %s%%', 'gridsmith' ), $value ), 'clear' => __( 'removed', 'gridsmith' ), ); return array( 'target' => $target, 'mode' => $mode, 'value' => (float) $value, 'label' => $labels[ $target ] . ' ' . $signs[ $mode ], ); } /** * New price for one product. * * @param WC_Product $product Product. * @param array $params Sanitised params. * @return array|null field => value, or null to skip. */ public static function price_compute( $product, array $params ) { if ( ! BDBE_Entity_Products::has_own_price( $product ) ) { return null; } $target = $params['target']; $value = (float) $params['value']; $regular = $product->get_regular_price( 'edit' ); $base = 'regular_price' === $target ? $regular : $product->get_sale_price( 'edit' ); switch ( $params['mode'] ) { case 'set': $new = $value; break; case 'clear': return array( $target => '' ); case 'regular_minus_percent': if ( '' === $regular ) { return null; } $new = (float) $regular * ( 1 - $value / 100 ); break; default: if ( '' === $base ) { return null; } $base = (float) $base; if ( 'increase_percent' === $params['mode'] ) { $new = $base * ( 1 + $value / 100 ); } elseif ( 'decrease_percent' === $params['mode'] ) { $new = $base * ( 1 - $value / 100 ); } elseif ( 'increase_amount' === $params['mode'] ) { $new = $base + $value; } else { $new = $base - $value; } } $decimals = wc_get_price_decimals(); $new = max( 0, round( $new, $decimals ) ); return array( $target => wc_format_decimal( $new, $decimals ) ); } /** * Run one chunk of a bulk action. * * @param string $action Action name. * @param array $params Raw params. * @param array $scope { ids: int[] } or { filters: array }. * @param int $offset Offset in the matching list. * @param string $token Batch token ('' starts a new batch). * @param string $context bulk|schedule. * @return array|WP_Error */ public static function run( $action, array $params, array $scope, $offset, $token, $context = 'bulk' ) { $actions = self::actions(); if ( ! isset( $actions[ $action ] ) ) { return new WP_Error( 'bdbe_action', __( 'Unknown bulk action.', 'gridsmith' ), array( 'status' => 400 ) ); } $def = $actions[ $action ]; $params = call_user_func( $def['params'], $params ); if ( is_wp_error( $params ) ) { return $params; } $entity = BDBE_Plugin::entity( 'product' ); if ( ! empty( $scope['ids'] ) ) { $ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $scope['ids'] ) ) ) ); } else { $ids = $entity->matching_ids( isset( $scope['filters'] ) ? (array) $scope['filters'] : array() ); } $offset = max( 0, (int) $offset ); $token = '' !== $token ? $token : wp_generate_uuid4(); $total = count( $ids ); $chunk = array_slice( $ids, $offset, self::CHUNK ); $changes = array(); $names = array(); $skipped = 0; foreach ( $chunk as $id ) { $product = $entity->load( $id ); if ( ! $product ) { ++$skipped; continue; } $targets = array( $product ); if ( $product->is_type( 'variable' ) ) { /** * Variations to process in place of a variable product (Pro). * * @param WC_Product[] $variations Variations (empty in Free). * @param WC_Product $product Variable product. * @param array $scope Scope incl. include_variations. */ $targets = (array) apply_filters( 'bdbe_bulk_expand', array(), $product, $scope ); if ( ! $targets && ! empty( $def['prices'] ) ) { ++$skipped; continue; } if ( ! $targets ) { $targets = array( $product ); } } foreach ( $targets as $target ) { $fields = call_user_func( $def['compute'], $target, $params ); if ( null === $fields || is_wp_error( $fields ) || ! $fields ) { if ( null === $fields && ! empty( $def['prices'] ) ) { ++$skipped; } continue; } $changes[ $target->get_id() ] = $fields; $names[ $target->get_id() ] = $target->get_name(); } } $result = BDBE_Changes::apply( $entity, $changes, array( 'label' => $params['label'], 'token' => $token, 'context' => $context, ) ); $errors = array(); foreach ( $result['errors'] as $id => $messages ) { $name = isset( $names[ $id ] ) ? $names[ $id ] : '#' . $id; $errors[] = $name . ': ' . implode( ' ', $messages ); } $next = $offset + count( $chunk ); return array( 'token' => $token, 'total' => $total, 'processed' => count( $chunk ), 'changed' => count( $result['saved'] ), 'skipped' => $skipped, 'errors' => $errors, 'next_offset' => $next < $total ? $next : null, 'label' => $params['label'], ); } }