PluginProbe ʕ •ᴥ•ʔ
WooCommerce Square / 2.2.5
WooCommerce Square v2.2.5
5.5.0 5.4.3 5.4.2 5.4.1 5.4.0 trunk 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.7.0 2.8.0 2.9.0 2.9.1 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.2.0 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.7.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 4.0.0 4.1.0 4.2.0 4.2.1 4.2.2 4.2.3 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.4.2 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.9.0 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 4.9.9 5.0.0 5.0.1 5.1.0 5.1.1 5.1.2 5.2.0 5.3.0 5.3.1 5.3.2 5.3.3
woocommerce-square / includes / Handlers / Products.php
woocommerce-square / includes / Handlers Last commit date
Product 6 years ago Background_Job.php 6 years ago Category.php 6 years ago Connection.php 6 years ago Email.php 6 years ago Order.php 6 years ago Product.php 5 years ago Products.php 5 years ago Sync.php 5 years ago
Products.php
731 lines
1 <?php
2 /**
3 * WooCommerce Square
4 *
5 * This source file is subject to the GNU General Public License v3.0
6 * that is bundled with this package in the file license.txt.
7 * It is also available through the world-wide-web at this URL:
8 * http://www.gnu.org/licenses/gpl-3.0.html
9 * If you did not receive a copy of the license and are unable to
10 * obtain it through the world-wide-web, please send an email
11 * to license@woocommerce.com so we can send you a copy immediately.
12 *
13 * DISCLAIMER
14 *
15 * Do not edit or add to this file if you wish to upgrade WooCommerce Square to newer
16 * versions in the future. If you wish to customize WooCommerce Square for your
17 * needs please refer to https://docs.woocommerce.com/document/woocommerce-square/
18 *
19 * @author WooCommerce
20 * @copyright Copyright: (c) 2019, Automattic, Inc.
21 * @license http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
22 */
23
24 namespace WooCommerce\Square\Handlers;
25
26 defined( 'ABSPATH' ) || exit;
27
28 use SkyVerge\WooCommerce\PluginFramework\v5_4_0 as Framework;
29 use WooCommerce\Square\Handlers\Product;
30 use WooCommerce\Square\Sync\Records;
31 use WooCommerce\Square;
32
33 /**
34 * Products admin handler.
35 *
36 * @since 2.0.0
37 */
38 class Products {
39
40
41 /** @var array associative array of product error codes and messages */
42 private $product_errors;
43
44 /** @var array associative array of memoized errors being output for a product at one time */
45 private $output_errors = array();
46
47 /** @var int[] array of product IDs that have been scheduled for sync in this request */
48 private $products_to_sync = array();
49
50 /** @var int[] array of product IDs that have been scheduled for deletion in this request */
51 private $products_to_delete = array();
52
53 /** @var Square\Plugin plugin instance */
54 private $plugin;
55
56 /**
57 * Sets up the products admin handler.
58 *
59 * @since 2.0.0
60 */
61 public function __construct( Square\Plugin $plugin ) {
62
63 $this->plugin = $plugin;
64
65 // add common errors
66 $this->product_errors = array(
67 /* translators: Placeholder: %s - product name */
68 'missing_sku' => __( "Please add an SKU to sync %s with Square. The SKU must match the item's SKU in your Square account.", 'woocommerce-square' ),
69 /* translators: Placeholder: %s - product name */
70 'missing_variation_sku' => __( "Please add an SKU to every variation of %s for syncing with Square. Each SKU must be unique and match the corresponding item's SKU in your Square account.", 'woocommerce-square' ),
71 /* translators: Placeholder: %s - product name */
72 'multiple_attributes' => __( '%s has multiple variation attributes and cannot be synced with Square.', 'woocommerce-square' ),
73 );
74
75 // add hooks
76 $this->add_products_edit_screen_hooks();
77 $this->add_product_edit_screen_hooks();
78 $this->add_product_sync_hooks();
79 }
80
81
82 /**
83 * Adds hooks to the admin products edit screen.
84 *
85 * Products filtering, bulk actions, etc.
86 *
87 * @since 2.0.0
88 */
89 private function add_products_edit_screen_hooks() {
90
91 // adds an option to the "Filter by product type" dropdown
92 add_action( 'restrict_manage_posts', array( $this, 'add_filter_products_synced_with_square_option' ) );
93 // allow filtering products by sync status by altering results
94 add_filter( 'request', array( $this, 'filter_products_synced_with_square' ) );
95
96 // prevent copying Square data when duplicating a product automatically
97 add_action( 'woocommerce_product_duplicate', array( $this, 'handle_product_duplication' ), 20, 2 );
98
99 // handle quick/bulk edit actions in the products edit screen for setting sync status
100 add_action( 'woocommerce_product_quick_edit_end', array( $this, 'add_quick_edit_inputs' ) );
101 add_action( 'woocommerce_product_bulk_edit_end', array( $this, 'add_bulk_edit_inputs' ) );
102 add_action( 'woocommerce_product_quick_edit_save', array( $this, 'set_synced_with_square' ) );
103 add_action( 'woocommerce_product_bulk_edit_save', array( $this, 'set_synced_with_square' ) );
104 }
105
106
107 /**
108 * Adds hooks to individual products edit screens.
109 *
110 * Product data input fields, variations, etc.
111 *
112 * @since 2.0.0
113 */
114 private function add_product_edit_screen_hooks() {
115
116 add_action( 'woocommerce_variation_options', array( $this, 'add_variation_manage_stock' ) );
117
118 // handle individual products input fields for setting sync status
119 add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_product_data_fields' ) );
120 add_action( 'woocommerce_admin_process_product_object', array( $this, 'process_product_data' ), 20 );
121 add_action( 'woocommerce_before_product_object_save', array( $this, 'maybe_adjust_square_stock' ) );
122
123 add_action( 'admin_notices', array( $this, 'add_notice_product_hidden_from_catalog' ) );
124 }
125
126
127 /**
128 * Adds our own hidden "manage stock" input to the variation fields.
129 *
130 * We disable the core checkbox, but this causes stock management to be disabled for the variations because the
131 * disabled field doesn't get POSTed. This overrides the checkbox value so that we can still disable it in the UI.
132 *
133 * @since 2.0.2
134 *
135 * @param int $loop currently looped variation
136 */
137 public function add_variation_manage_stock( $loop ) {
138
139 if ( ! wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
140 return;
141 }
142
143 ?> <input type="hidden" id="wc_square_variation_manage_stock" name="variable_manage_stock[<?php echo esc_attr( $loop ); ?>]" value="1" />
144 <?php
145 }
146
147
148 /**
149 * Adds hooks to sync products that have been updated.
150 *
151 * @since 2.0.0
152 */
153 private function add_product_sync_hooks() {
154
155 add_action( 'woocommerce_update_product', array( $this, 'validate_product_update_and_sync' ) );
156 add_action( 'trashed_post', array( $this, 'maybe_stage_products_for_deletion' ) );
157 add_action( 'shutdown', array( $this, 'maybe_sync_staged_products' ) );
158 add_action( 'shutdown', array( $this, 'maybe_delete_staged_products' ) );
159 }
160
161
162 /**
163 * Adds an option to filter products by sync status.
164 *
165 * @internal
166 *
167 * @since 2.0.0
168 *
169 * @param string $post_type the post type context
170 */
171 public function add_filter_products_synced_with_square_option( $post_type ) {
172
173 if ( 'product' !== $post_type ) {
174 return;
175 }
176
177 $label = esc_html__( 'Synced with Square', 'woocommerce-square' );
178 $selected = isset( $_GET['product_type'] ) && 'synced-with-square' === $_GET['product_type'] ? 'selected=\"selected\"' : '';
179
180 wc_enqueue_js(
181 "
182 jQuery( document ).ready( function( $ ) {
183 $( 'select#dropdown_product_type' ) . append( '<option value=\"synced-with-square\" ' + '" . $selected . "' + '>' + '" . $label . "' + '</option>' );
184 } );
185 "
186 );
187 }
188
189
190 /**
191 * Filters products in admin edit screen by sync status with Square.
192 *
193 * @internal
194 *
195 * @since 2.0.0
196 *
197 * @param array $query_vars query variables
198 * @return array
199 */
200 public function filter_products_synced_with_square( $query_vars ) {
201 global $typenow;
202
203 if ( 'product' === $typenow && isset( $_GET['product_type'] ) && 'synced-with-square' === $_GET['product_type'] ) {
204
205 // not really a product type, otherwise WooCommerce will handle it as such
206 unset( $query_vars['product_type'] );
207
208 if ( ! isset( $query_vars['tax_query'] ) ) {
209 $query_vars['tax_query'] = array();
210 } else {
211 $query_vars['tax_query']['relation'] = 'AND';
212 }
213
214 $query_vars['tax_query'][] = array(
215 'taxonomy' => Product::SYNCED_WITH_SQUARE_TAXONOMY,
216 'field' => 'slug',
217 'terms' => array( 'yes' ),
218 );
219 }
220
221 return $query_vars;
222 }
223
224
225 /**
226 * Adds general product data options to a product metabox.
227 *
228 * @internal
229 *
230 * @since 2.0.0
231 */
232 public function add_product_data_fields() {
233 global $product_object;
234
235 if ( ! $product_object instanceof \WC_Product ) {
236 return;
237 }
238
239 // don't show fields if product sync is disabled
240 if ( ! wc_square()->get_settings_handler()->is_product_sync_enabled() ) {
241 return;
242 }
243
244 ?>
245 <div class="wc-square-sync-with-square options_group show_if_simple show_if_variable">
246 <?php
247
248 $selector = '_' . Product::SYNCED_WITH_SQUARE_TAXONOMY;
249 $value = Product::is_synced_with_square( $product_object ) ? 'yes' : 'no';
250 $errors = $this->check_product_sync_errors( $product_object );
251
252 $setting_label = wc_square()->get_settings_handler()->is_system_of_record_square() ? __( 'Update product data with Square data', 'woocommerce-square' ) : __( 'Send product data to Square', 'woocommerce-square' );
253
254 woocommerce_wp_checkbox(
255 array(
256 'id' => $selector,
257 'label' => __( 'Sync with Square', 'woocommerce-square' ),
258 'value' => $value,
259 'cbvalue' => 'yes',
260 'default' => 'no',
261 'description' => $setting_label,
262 'custom_attributes' => ! empty( $errors ) ? array( 'disabled' => 'disabled' ) : array(),
263 )
264 );
265
266 ?>
267 <p class="form-field wc-square-sync-with-square-errors">
268 <?php foreach ( $this->product_errors as $error_code => $error_message ) : ?>
269 <?php $styles = ! in_array( $error_code, array_keys( $errors ), true ) ? 'display:none; color:#A00;' : 'display:block; color:#A00;'; ?>
270 <span class="wc-square-sync-with-square-error <?php echo sanitize_html_class( $error_code ); ?>" style="<?php echo $styles; ?>"><?php echo $this->format_product_error( $error_code, $product_object ); ?></span>
271 <?php endforeach; ?>
272 </p>
273
274 <input type="hidden" id="<?php echo esc_attr( Product::SQUARE_VARIATION_ID_META_KEY ); ?>" value="<?php echo esc_attr( $product_object->get_meta( Product::SQUARE_VARIATION_ID_META_KEY ) ); ?>" />
275
276 </div>
277 <?php
278 }
279
280
281 /**
282 * Outputs HTML with a dropdown field to mark a product to be synced with Square.
283 *
284 * @since 2.0.0
285 *
286 * @param bool $bulk whether the field is meant for bulk edit
287 */
288 private function output_synced_with_square_edit_field( $bulk = false ) {
289
290 ?>
291 <div class="inline-edit-group wc-square-sync-with-square">
292 <label>
293 <span class="title"><?php esc_html_e( 'Sync with Square?', 'woocommerce-square' ); ?></span>
294 <span class="input-text-wrap">
295 <select class="square-synced" name="<?php echo esc_attr( Product::SYNCED_WITH_SQUARE_TAXONOMY ); ?>">
296 <?php if ( true === $bulk ) : // in bulk actions there's the option to leave the value unchanged (or unset) ?>
297 <option value="">&mdash; <?php esc_html_e( 'No change', 'woocommerce-square' ); ?> &mdash;</option>
298 <?php endif; ?>
299 <option value="no"><?php esc_html_e( 'No', 'woocommerce-square' ); ?></option>
300 <option value="yes"><?php esc_html_e( 'Yes', 'woocommerce-square' ); ?></option>
301 </select>
302 </span>
303 </label>
304 <p class="form-field wc-square-sync-with-square-errors">
305 <?php foreach ( $this->product_errors as $error_code => $error_message ) : ?>
306 <?php $product_name_placeholder = __( 'This product', 'woocommerce-square' ); ?>
307 <?php $product_name = ( 'multiple_attributes' !== $error_code ) ? strtolower( $product_name_placeholder ) : $product_name_placeholder; ?>
308 <span class="wc-square-sync-with-square-error <?php echo $error_code; ?>" style="display:none; color:#A00;"><?php echo esc_html( sprintf( $error_message, $product_name ) ); ?></span>
309 <?php endforeach; ?>
310 </p>
311 </div>
312 <?php
313 }
314
315
316 /**
317 * Adds quick edit fields to the products screen.
318 *
319 * @internal
320 *
321 * @since 2.0.0
322 */
323 public function add_quick_edit_inputs() {
324
325 $this->output_synced_with_square_edit_field();
326 }
327
328
329 /**
330 * Adds bulk edit fields to the products screen.
331 *
332 * @internal
333 *
334 * @since 2.0.0
335 */
336 public function add_bulk_edit_inputs() {
337
338 $this->output_synced_with_square_edit_field( true );
339 }
340
341
342 /**
343 * In case Woo is the SOR, validates whether a product can be synced with Square and disable sync if not
344 *
345 * @since 2.0.8
346 *
347 * @param int $product_id the product ID
348 */
349 public function validate_product_update_and_sync( $product_id ) {
350 if ( ! wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
351 return;
352 }
353
354 $product = wc_get_product( $product_id );
355
356 if ( ! Product::is_synced_with_square( $product ) ) {
357 return;
358 }
359
360 $errors = $this->check_product_sync_errors( $product );
361
362 if ( ! empty( $errors ) ) {
363 // if there are errors, remove the link and display them
364 Product::unset_synced_with_square( $product );
365
366 foreach ( $errors as $error ) {
367 wc_square()->get_message_handler()->add_error( $error );
368 Records::set_record(
369 array(
370 'type' => 'alert',
371 'product_id' => $product_id,
372 'message' => $error,
373 )
374 );
375 }
376 } else {
377 $this->maybe_stage_product_for_sync( $product );
378 }
379 }
380
381 /**
382 * Stages a product for sync with Square on product save if Woo is the SOR and the product is set to 'synced with square'.
383 *
384 * @internal
385 *
386 * @since 2.0.0
387 *
388 * @param WC_Product $product the product object
389 */
390 public function maybe_stage_product_for_sync( $product ) {
391 if ( ! defined( 'DOING_SQUARE_SYNC' ) && ! in_array( $product->get_id(), $this->products_to_sync ) ) {
392
393 if ( $product && Product::is_synced_with_square( $product ) ) {
394
395 // the triggering action for this method can be called multiple times in a single request - keep track
396 // of product IDs that have been scheduled for sync here to avoid multiple syncs on the same request
397 $this->products_to_sync[] = $product->get_id();
398 }
399 }
400 }
401
402
403 /**
404 * Initializes a synchronization event for any staged products in this request.
405 *
406 * @internal
407 *
408 * @since 2.0.0
409 */
410 public function maybe_sync_staged_products() {
411
412 if ( ! defined( 'DOING_SQUARE_SYNC' ) && ! empty( $this->products_to_sync ) && wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
413
414 wc_square()->get_sync_handler()->start_manual_sync( true, $this->products_to_sync );
415 }
416 }
417
418
419 /**
420 * Removes a product from Square if it is deleted locally and Woo is the SOR.
421 *
422 * @since 2.0.0
423 *
424 * @param int $product_id the product ID
425 */
426 public function maybe_stage_products_for_deletion( $product_id ) {
427
428 if ( wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
429
430 $product = wc_get_product( $product_id );
431
432 if ( $product && Product::is_synced_with_square( $product ) ) {
433
434 // the triggering action for this method can be called multiple times in a single request - keep track
435 // of product IDs that have been scheduled for sync here to avoid multiple syncs on the same request
436 $this->products_to_delete[] = $product_id;
437 }
438 }
439 }
440
441
442 /**
443 * Deletes any products staged for remote deletion.
444 *
445 * @since 2.0.0
446 */
447 public function maybe_delete_staged_products() {
448
449 if ( ! empty( $this->products_to_delete ) && wc_square()->get_settings_handler()->is_system_of_record_woocommerce() ) {
450
451 wc_square()->get_sync_handler()->start_manual_deletion( $this->products_to_delete );
452 }
453 }
454
455
456 /**
457 * Sets a product's synced with Square status for quick/bulk edit action.
458 *
459 * @internal
460 *
461 * @since 2.0.0
462 *
463 * @param \WC_Product $product a product object
464 */
465 public function set_synced_with_square( $product ) {
466
467 $posted_data_key = Product::SYNCED_WITH_SQUARE_TAXONOMY;
468
469 if ( 'woocommerce_product_bulk_edit_save' === current_action() ) {
470 $default_value = null; // in bulk actions this will preserve the existing setting if nothing is specified
471 } else {
472 $default_value = 'no'; // in individual products context, the value should be always an explicit yes or no
473 }
474
475 $square_synced = isset( $_REQUEST[ $posted_data_key ] ) && in_array( $_REQUEST[ $posted_data_key ], array( 'yes', 'no' ), true ) ? $_REQUEST[ $posted_data_key ] : $default_value;
476
477 if ( is_string( $square_synced ) ) {
478 $errors = $this->check_product_sync_errors( $product );
479 if ( 'no' === $square_synced || empty( $errors ) ) {
480 Product::set_synced_with_square( $product, $square_synced );
481 } elseif ( ! empty( $errors ) ) {
482 foreach ( $errors as $error ) {
483 wc_square()->get_message_handler()->add_error( $error );
484 }
485 }
486 }
487 }
488
489
490 /**
491 * Updates Square sync status for a product upon saving.
492 *
493 * @internal
494 *
495 * @since 2.0.0
496 *
497 * @param \WC_Product $product product object
498 */
499 public function process_product_data( $product ) {
500
501 // don't process fields if product sync is disabled
502 if ( ! wc_square()->get_settings_handler()->is_product_sync_enabled() ) {
503 return;
504 }
505
506 // bail if no valid product found, if it's a variation, errors have already been output
507 if ( ! $product || ( $product instanceof \WC_Product_Variation || $product->is_type( 'product_variation' ) ) || ! empty( $this->output_errors[ $product->get_id() ] ) ) {
508 return;
509 }
510
511 $errors = array();
512 $posted_key = '_' . Product::SYNCED_WITH_SQUARE_TAXONOMY;
513 $set_synced = isset( $_POST[ $posted_key ] ) && 'yes' === $_POST[ $posted_key ];
514 $was_synced = Product::is_synced_with_square( $product );
515
516 // condition has unchanged
517 if ( ! $set_synced && ! $was_synced ) {
518 return;
519 }
520
521 if ( $set_synced || $was_synced ) {
522 if ( $set_synced && $product->is_type( 'variable' ) && wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
523 // if syncing inventory with Square, parent variable products don't manage stock
524 $product->set_manage_stock( false );
525
526 // if there are no errors, and the product is variable, force enable stock management for all its variations
527 foreach ( $product->get_children() as $variation_id ) {
528 if ( $variation = wc_get_product( $variation_id ) ) {
529 $variation->set_manage_stock( true );
530 $variation->save();
531 }
532 }
533 }
534
535 // finally, set the product sync with Square flag
536 Product::set_synced_with_square( $product, $set_synced ? 'yes' : 'no' );
537 }
538 }
539
540
541 /**
542 * Adjusts a product's Square stock.
543 *
544 * @since 2.0.0
545 *
546 * @param \WC_Product $product product object
547 */
548 public function maybe_adjust_square_stock( $product ) {
549
550 // this is hooked in to general product object save, so scope to specifically saving products via the admin
551 if ( ! doing_action( 'wp_ajax_woocommerce_save_variations' ) && ! doing_action( 'woocommerce_admin_process_product_object' ) ) {
552 return;
553 }
554
555 // only send stock updates for Woo SOR
556 if ( ! wc_square()->get_settings_handler()->is_system_of_record_woocommerce() || ! wc_square()->get_settings_handler()->is_inventory_sync_enabled() ) {
557 return;
558 }
559
560 if ( ! $product instanceof \WC_Product || ! Product::is_synced_with_square( $product ) ) {
561 return;
562 }
563
564 $square_id = $product->get_meta( Product::SQUARE_VARIATION_ID_META_KEY );
565
566 // only send when the product has an associated Square ID
567 if ( ! $square_id ) {
568 return;
569 }
570
571 // set to manage stock if not a variable product
572 $product->set_manage_stock( ! $product->is_type( 'variable' ) );
573
574 $data = $product->get_data();
575 $changes = $product->get_changes();
576 $change = 0;
577
578 if ( isset( $data['stock_quantity'], $changes['stock_quantity'] ) ) {
579 $change = (int) $changes['stock_quantity'] - $data['stock_quantity'];
580 }
581
582 if ( $change !== 0 ) {
583
584 try {
585
586 if ( $change > 0 ) {
587 wc_square()->get_api()->add_inventory( $square_id, $change );
588 } else {
589 wc_square()->get_api()->remove_inventory( $square_id, $change );
590 }
591 } catch ( Framework\SV_WC_Plugin_Exception $exception ) {
592
593 wc_square()->log( 'Could not adjust Square inventory for ' . $product->get_formatted_name() . '. ' . $exception->getMessage() );
594
595 $quantity = (float) $data['stock_quantity'];
596
597 // if the API request fails, set the product quantity back from whence it came
598 $product->set_stock_quantity( $quantity );
599 }
600 }
601 }
602
603
604 /**
605 * Prevents copying Square data when duplicating a product in admin.
606 *
607 * @internal
608 *
609 * @since 2.0.0
610 *
611 * @param \WC_Product $duplicated_product product duplicate
612 * @param \WC_Product $original_product product duplicated
613 */
614 public function handle_product_duplication( $duplicated_product, $original_product ) {
615
616 if ( Product::is_synced_with_square( $original_product ) ) {
617 Product::unset_synced_with_square( $duplicated_product );
618 }
619
620 $duplicated_product->delete_meta_data( Product::SQUARE_ID_META_KEY );
621 $duplicated_product->delete_meta_data( Product::SQUARE_VARIATION_ID_META_KEY );
622
623 if ( $duplicated_product->is_type( 'variable' ) ) {
624
625 foreach ( $duplicated_product->get_children() as $duplicated_variation_id ) {
626
627 if ( $duplicated_product_variation = wc_get_product( $duplicated_variation_id ) ) {
628
629 $duplicated_product_variation->delete_meta_data( Product::SQUARE_VARIATION_ID_META_KEY );
630 $duplicated_product_variation->save_meta_data();
631 }
632 }
633 }
634
635 $duplicated_product->save_meta_data();
636 }
637
638
639 /**
640 * Outputs an admin notice when a product was hidden from catalog upon a sync error.
641 *
642 * @internal
643 *
644 * @since 2.0.0
645 */
646 public function add_notice_product_hidden_from_catalog() {
647 global $current_screen, $post;
648
649 if ( $post && $current_screen && 'product' === $current_screen->id ) {
650
651 $product = wc_get_product( $post );
652
653 if ( $product && 'hidden' === $product->get_catalog_visibility() ) {
654
655 $product_id = $product->get_id();
656 $records = Records::get_records( array( 'product' => $product_id ) );
657
658 foreach ( $records as $record ) {
659
660 if ( $record->was_product_hidden() && $product_id === $record->get_product_id() ) {
661
662 wc_square()->get_message_handler()->add_warning(
663 sprintf(
664 /* translators: Placeholder: %1$s - date (localized), %2$s - time (localized), %3$s - opening <a> HTML link tag, %4$s closing </a> HTML link tag */
665 esc_html__( 'The product catalog visibility has been set to "hidden", as a matching product could not be found in Square on %1$s at %2$s. %3$sCheck sync records%4$s.', 'woocommerce-square' ),
666 date_i18n( wc_date_format(), $record->get_timestamp() ),
667 date_i18n( wc_time_format(), $record->get_timestamp() ),
668 '<a href="' . esc_url( add_query_arg( array( 'section' => 'update' ), wc_square()->get_settings_url() ) ) . '">',
669 '</a>'
670 )
671 );
672
673 break;
674 }
675 }
676 }
677 }
678 }
679
680
681 /**
682 * Check whether this product can be synced with Square
683 *
684 * @param \WC_Product $product product object
685 * @return array errors
686 */
687 private function check_product_sync_errors( \WC_Product $product ) {
688 $errors = array();
689 if ( $product->is_type( 'variable' ) && $product->has_child() ) {
690 if ( Product::has_multiple_variation_attributes( $product ) ) {
691 $errors['multiple_attributes'] = $this->format_product_error( 'multiple_attributes', $product );
692 } elseif ( ! Product::has_sku( $product ) ) {
693 $errors['missing_variation_sku'] = $this->format_product_error( 'missing_variation_sku', $product );
694 }
695 } else {
696 if ( ! Product::has_sku( $product ) ) {
697 $errors['missing_sku'] = $this->format_product_error( 'missing_sku', $product );
698 }
699 }
700 return $errors;
701 }
702
703
704 /**
705 * Formats product error message with product information
706 *
707 * @param string $error error identifier (e.g. 'multiple_attributes', 'missing_variation_sku' or 'missing_sku')
708 * @param \WC_Product $product product object
709 * @return string formatted error message
710 */
711 private function format_product_error( string $error, \WC_Product $product ) {
712 return sprintf(
713 $this->product_errors[ $error ],
714 Product::get_product_edit_link( $product )
715 );
716 }
717
718
719 /**
720 * Gets the plugin instance.
721 *
722 * @since 2.0.8
723 *
724 * @return Plugin
725 */
726 protected function get_plugin() {
727 return $this->plugin;
728 }
729
730 }
731