/includes/api/class-wc-pos-products.php

  1. <?php 
  2.  
  3. /** 
  4. * POS Product Class 
  5. * duck punches the WC REST API 
  6. * 
  7. * @class WC_POS_API_Products 
  8. * @package WooCommerce POS 
  9. * @author Paul Kilmurray <paul@kilbot.com.au> 
  10. * @link http://www.woopos.com.au 
  11. */ 
  12.  
  13. class WC_POS_API_Products extends WC_POS_API_Abstract { 
  14.  
  15. /** 
  16. * Product fields used by the POS 
  17. * @var array 
  18. */ 
  19. private $whitelist = array( 
  20. 'title',  
  21. 'name',  
  22. 'id',  
  23. 'created_at',  
  24. 'updated_at',  
  25. 'type',  
  26. 'status',  
  27. 'downloadable',  
  28. 'virtual',  
  29. // 'permalink',  
  30. 'sku',  
  31. 'price',  
  32. 'regular_price',  
  33. 'sale_price',  
  34. 'price_html',  
  35. 'taxable',  
  36. 'tax_status',  
  37. 'tax_class',  
  38. 'managing_stock',  
  39. 'stock_quantity',  
  40. 'in_stock',  
  41. 'backorders_allowed',  
  42. 'backordered',  
  43. 'sold_individually',  
  44. 'purchaseable',  
  45. 'featured',  
  46. 'visible',  
  47. // 'catalog_visibility',  
  48. 'on_sale',  
  49. // 'weight',  
  50. // 'dimensions',  
  51. 'shipping_required',  
  52. 'shipping_taxable',  
  53. 'shipping_class',  
  54. 'shipping_class_id',  
  55. // 'description',  
  56. // 'short_description',  
  57. // 'reviews_allowed',  
  58. // 'average_rating',  
  59. // 'rating_count',  
  60. // 'related_ids',  
  61. // 'upsell_ids',  
  62. // 'cross_sell_ids',  
  63. 'parent_id',  
  64. 'categories',  
  65. // 'tags',  
  66. // 'images',  
  67. 'featured_src',  
  68. 'attributes',  
  69. // 'downloads',  
  70. // 'download_limit',  
  71. // 'download_expiry',  
  72. // 'download_type',  
  73. 'purchase_note',  
  74. 'total_sales',  
  75. 'variations',  
  76. // 'parent',  
  77.  
  78. /** 
  79. * Fields add by POS 
  80. * - product thumbnail 
  81. * - barcode 
  82. */ 
  83. 'featured_src',  
  84. 'barcode' 
  85. ); 
  86.  
  87. /** 
  88. * 
  89. */ 
  90. public function __construct() { 
  91. add_filter( 'woocommerce_api_product_response', array( $this, 'product_response' ), 10, 4 ); 
  92. add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); 
  93. add_filter( 'posts_where', array( $this, 'posts_where' ), 10 , 2 ); 
  94.  
  95. /** 
  96. * Filter each product response from WC REST API for easier handling by the POS 
  97. * - use the thumbnails rather than fullsize 
  98. * - add barcode field 
  99. * - unset unnecessary data 
  100. * 
  101. * @param array $data 
  102. * @param $product 
  103. * 
  104. * @return array modified data array $product_data 
  105. */ 
  106. public function product_response( $data, $product, $fields, $server ) { 
  107. $type = isset( $data['type'] ) ? $data['type'] : ''; 
  108.  
  109. // variable products 
  110. if( $type == 'variable' ) : 
  111. // nested variations 
  112. foreach( $data['variations'] as &$variation ) : 
  113. $_product = wc_get_product( $variation['id'] ); 
  114. $variation = $this->filter_response_data( $variation, $_product ); 
  115. $variation['attributes'] = $this->patch_variation_attributes( $_product ); 
  116. endforeach; 
  117. endif; 
  118.  
  119. // variation 
  120. if( $type == 'variation' ) : 
  121. $data['attributes'] = $this->patch_variation_attributes( $product ); 
  122. endif; 
  123.  
  124. return $this->filter_response_data( $data, $product ); 
  125.  
  126. /** 
  127. * https://github.com/woothemes/woocommerce/issues/8457 
  128. * patches WC_Product_Variable->get_variation_attributes() 
  129. * @param $product 
  130. * @return array 
  131. */ 
  132. private function patch_variation_attributes( $product ) { 
  133. $patched_attributes = array(); 
  134. $attributes = $product->get_attributes(); 
  135. $variation_attributes = $product->get_variation_attributes(); 
  136.  
  137. // patch for corrupted data, depreciate asap 
  138. if( empty( $attributes ) ) { 
  139. $attributes = $product->parent->product_attributes; 
  140. delete_post_meta( $product->variation_id, '_product_attributes' ); 
  141.  
  142. foreach( $variation_attributes as $slug => $option ) { 
  143. $slug = str_replace( 'attribute_', '', $slug ); 
  144.  
  145. if( isset( $attributes[$slug] ) ) { 
  146. $patched_attributes[] = array( 
  147. 'name' => $this->get_variation_name( $attributes[$slug] ),  
  148. 'option' => $this->get_variation_option( $product, $attributes[$slug], $option ) 
  149. ); 
  150.  
  151.  
  152. return $patched_attributes; 
  153.  
  154. /** 
  155. * @param $attribute 
  156. * @return null|string 
  157. */ 
  158. private function get_variation_name( $attribute ) { 
  159. if( $attribute['is_taxonomy'] ) { 
  160. global $wpdb; 
  161. $name = str_replace( 'pa_', '', $attribute['name'] ); 
  162.  
  163. $label = $wpdb->get_var( 
  164. $wpdb->prepare(" 
  165. SELECT attribute_label 
  166. FROM {$wpdb->prefix}woocommerce_attribute_taxonomies 
  167. WHERE attribute_name = %s; 
  168. ", $name ) ); 
  169.  
  170. return $label ? $label : $name; 
  171.  
  172. return $attribute['name']; 
  173.  
  174. /** 
  175. * @param $product 
  176. * @param $option 
  177. * @param $attribute 
  178. * @return mixed 
  179. */ 
  180. private function get_variation_option( $product, $attribute, $option ) { 
  181. $name = $option; 
  182.  
  183. // taxonomy attributes 
  184. if ( $attribute['is_taxonomy'] ) { 
  185. $terms = wp_get_post_terms( $product->parent->id, $attribute['name'] ); 
  186. if( !is_wp_error($terms) ) : foreach( $terms as $term ) : 
  187. if( $option === $term->slug ) $name = $term->name; 
  188. endforeach; endif; 
  189.  
  190. // piped attributes 
  191. } else { 
  192. $values = array_map( 'trim', explode( WC_DELIMITER, $attribute['value'] ) ); 
  193. $options = array_combine( array_map( 'sanitize_title', $values) , $values ); 
  194. if( $options && isset( $options[$option] ) ) { 
  195. $name = $options[$option]; 
  196.  
  197. return $name; 
  198.  
  199. /** 
  200. * Filter product response data 
  201. * - add featured_src 
  202. * - add special key for barcode, defaults to sku 
  203. * @param array $data 
  204. * @param $product 
  205. * @return array 
  206. */ 
  207. private function filter_response_data( array $data, $product ) { 
  208. $id = isset( $data['id'] ) ? $data['id'] : ''; 
  209. $barcode = isset( $data['sku'] ) ? $data['sku'] : ''; 
  210.  
  211. // allow custom barcode field 
  212. $barcode_meta_key = apply_filters( 'woocommerce_pos_barcode_meta_key', '_sku' ); 
  213. if( $barcode_meta_key !== '_sku' ) { 
  214. $barcode = get_post_meta( $id, $barcode_meta_key, true ); 
  215.  
  216. $data['featured_src'] = $this->get_thumbnail( $id ); 
  217. $data['barcode'] = apply_filters( 'woocommerce_pos_product_barcode', $barcode, $id ); 
  218.  
  219. // allow decimal stock quantities, fixed in WC 2.4 
  220. if( version_compare( WC()->version, '2.4', '<' ) ) { 
  221. $data['stock_quantity'] = $product->get_stock_quantity(); 
  222.  
  223. // Transition to name property 
  224. if( isset($data['title']) ) { 
  225. $data['name'] = $data['title']; 
  226. unset($data['title']); 
  227.  
  228. // filter by whitelist 
  229. // - note, this uses the same method as WC REST API fields parameter 
  230. // - this doesn't speed up queries as it should 
  231. // - when WC REST API properly filters requests POS should use fields param 
  232. return array_intersect_key( $data, array_flip( $this->whitelist ) ); 
  233.  
  234. /** 
  235. * Returns thumbnail if it exists, if not, returns the WC placeholder image 
  236. * @param int $id 
  237. * @return string 
  238. */ 
  239. private function get_thumbnail($id) { 
  240. $image = false; 
  241. $thumb_id = get_post_thumbnail_id( $id ); 
  242.  
  243. if( $thumb_id ) 
  244. $image = wp_get_attachment_image_src( $thumb_id, 'shop_thumbnail' ); 
  245.  
  246. if( is_array($image) ) 
  247. return $image[0]; 
  248.  
  249. return wc_placeholder_img_src(); 
  250.  
  251. /** 
  252. * @param $query 
  253. */ 
  254. public function pre_get_posts($query) { 
  255.  
  256. // store original meta_query 
  257. $meta_query = $query->get( 'meta_query' ); 
  258.  
  259. if( isset( $_GET['filter'] ) ) { 
  260.  
  261. $filter = $_GET['filter']; 
  262.  
  263. // barcode moved to posts_where for variation search 
  264. // if( isset($filter['barcode']) ) { 
  265. // $meta_query[] = array( 
  266. // 'key' => $this->barcode_meta_key,  
  267. // 'value' => $filter['barcode'],  
  268. // 'compare' => 'LIKE' 
  269. // ); 
  270. // } 
  271.  
  272. // featured 
  273. // todo: more general meta_key test using $query_args_whitelist 
  274. if( isset($filter['featured']) ) { 
  275. $meta_query[] = array( 
  276. 'key' => '_featured',  
  277. 'value' => $filter['featured'] ? 'yes' : 'no',  
  278. 'compare' => '=' 
  279. ); 
  280.  
  281. // on sale 
  282. // - no easy way to get on_sale items 
  283. // - wc_get_product_ids_on_sale uses cached data, includes variations 
  284. if( isset($filter['on_sale']) ) { 
  285. $sale_ids = array_filter( wc_get_product_ids_on_sale() ); 
  286. $exclude = isset($query->query['post__not_in']) ? $query->query['post__not_in'] : array(); 
  287. $ids = array_diff($sale_ids, $exclude); 
  288. $query->set( 'post__not_in', array() ); 
  289. $query->set( 'post__in', $ids ); 
  290.  
  291.  
  292. // order product alphabetically 
  293. $query->set('orderby', 'post_title'); 
  294. $query->set('order', 'ASC'); 
  295.  
  296. // update the meta_query 
  297. $query->set( 'meta_query', $meta_query ); 
  298.  
  299.  
  300. /** 
  301. * @param $where 
  302. * @param $query 
  303. * @return mixed 
  304. */ 
  305. public function posts_where( $where, $query ) { 
  306. global $wpdb; 
  307.  
  308. if( isset( $_GET['filter'] ) ) { 
  309.  
  310. $filter = $_GET['filter']; 
  311.  
  312. if( isset($filter['barcode']) ) { 
  313.  
  314. $barcode_meta_key = apply_filters( 'woocommerce_pos_barcode_meta_key', '_sku' ); 
  315.  
  316. // gets post ids and parent ids 
  317. $result = $wpdb->get_results( 
  318. $wpdb->prepare(" 
  319. SELECT p.ID, p.post_parent 
  320. FROM $wpdb->posts AS p 
  321. JOIN $wpdb->postmeta AS pm 
  322. ON p.ID = pm.post_id 
  323. WHERE pm.meta_key = %s 
  324. AND pm.meta_value LIKE %s 
  325. ", $barcode_meta_key, '%'.$filter['barcode'].'%' ),  
  326. ARRAY_N 
  327. ); 
  328.  
  329. if($result) { 
  330. $ids = call_user_func_array('array_merge', $result); 
  331. $where .= " AND ID IN (" . implode( ', ', array_unique($ids) ) . ")"; 
  332. } else { 
  333. // no matches 
  334. $where .= " AND 1=0"; 
  335.  
  336.  
  337.  
  338. return $where; 
  339.  
  340. /** 
  341. * Returns array of all product ids 
  342. * @param $updated_at_min 
  343. * @return array 
  344. */ 
  345. public function get_ids($updated_at_min) { 
  346. $args = array( 
  347. 'post_type' => array('product'),  
  348. 'post_status' => array('publish'),  
  349. 'posts_per_page'=> -1,  
  350. 'fields' => 'ids' 
  351. ); 
  352.  
  353. if($updated_at_min) { 
  354. $args['date_query'][] = array( 
  355. 'column' => 'post_modified_gmt',  
  356. 'after' => $updated_at_min,  
  357. 'inclusive' => false 
  358. ); 
  359.  
  360. $query = new WP_Query( $args ); 
  361. return array_map( 'intval', $query->posts ); 
  362.  
.