version, '3.5.0', '<' ) ) { // Add the button for adding the inbound stock products to the WC stock. add_action( 'atum/atum_order/item_bulk_controls', array( $this, 'add_stock_button' ) ); } // Add the button for setting the purchase price to products within POs. add_action( 'atum/atum_order/item_meta_controls', array( $this, 'set_purchase_price_button' ) ); // Add message before the PO product search. add_action( 'atum/atum_order/before_product_search_modal', array( $this, 'product_search_message' ) ); // Use the purchase price when adding products to a PO. add_filter( 'atum/order/add_product/price', array( $this, 'use_purchase_price' ), 10, 3 ); // Maybe change product stock when order status change. add_action( 'atum/orders/status_atum_received', array( $this, 'maybe_increase_stock_levels' ), 10, 2 ); add_action( 'atum/orders/status_changed', array( $this, 'maybe_decrease_stock_levels' ), 10, 4 ); parent::__construct( $id, $read_items ); $this->block_message = __( 'Set the Supplier field above in order to add/edit items.', ATUM_TEXT_DOMAIN ); } /** * Add the button for adding the inbound stock products to the WC stock * * @since 1.3.0 */ public function add_stock_button() { ?> get_type() ) : ?> get_supplier(); if ( $supplier ) { /* translators: the supplier title */ echo ' ' . sprintf( esc_attr__( "Only products linked to '%s' supplier can be searched.", ATUM_TEXT_DOMAIN ), esc_attr( $supplier->post_title ) ) . ''; } } /********* * GETTERS *********/ /** * Get the title for the PO post * * @since 1.2.9 * * @return string */ public function get_title() { // phpcs:ignore WordPress.WP.I18n.MissingArgDomain if ( ! empty( $this->post->post_title ) && __( 'Auto Draft', ATUM_TEXT_DOMAIN ) !== $this->post->post_title ) { $post_title = $this->post->post_title; } else { /* translators: the purchase order date */ $post_title = sprintf( __( 'PO – %s', ATUM_TEXT_DOMAIN ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'PO date parsed by strftime', ATUM_TEXT_DOMAIN ), strtotime( $this->get_date() ) ) ); // phpcs:ignore WordPress.WP.I18n.UnorderedPlaceholdersText } return apply_filters( 'atum/purchase_orders/po/title', $post_title ); } /** * Get the supplier associated to this PO * * @since 1.2.9 * * @param string $return Optional. The type of object to return. Possible values 'id' or 'post'. * * @return \WP_Post|int|bool */ public function get_supplier( $return = 'post' ) { $supplier_id = $this->get_meta( Suppliers::SUPPLIER_META_KEY ); if ( $supplier_id ) { if ( 'id' === $return ) { return $supplier_id; } else { $supplier = get_post( $supplier_id ); return $supplier; } } return FALSE; } /** * Check whether this PO allows products from multiple suppliers * * @since 1.4.2 * * @return bool */ public function has_multiple_suppliers() { return 'yes' === $this->get_meta( '_multiple_suppliers' ); } /** * Get the Order's type * * @since 1.4.16 * * @return string */ public function get_type() { return ATUM_PREFIX . 'purchase_order'; } /** * Get an ATUM Order item * * @since 1.2.9 * * @param object $item * * @return \WC_Order_Item|false if not found */ public function get_atum_order_item( $item = NULL ) { if ( is_a( $item, '\WC_Order_Item' ) ) { /** * Variable definition * * @var \WC_Order_Item $item */ $item_type = $item->get_type(); $id = $item->get_id(); } elseif ( is_object( $item ) && ! empty( $item->order_item_type ) ) { /* @noinspection PhpUndefinedFieldInspection */ $id = $item->order_item_id; $item_type = $item->order_item_type; } elseif ( is_numeric( $item ) && ! empty( $this->items ) ) { $id = $item; foreach ( $this->items as $group => $group_items ) { foreach ( $group_items as $item_id => $stored_item ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison if ( $id == $item_id ) { $item_type = $this->group_to_type( $group ); break 2; } } } } else { $item_type = FALSE; $id = FALSE; } if ( $id && isset( $item_type ) && $item_type ) { $classname = FALSE; $items_namespace = '\\Atum\\PurchaseOrders\\Items\\'; switch ( $item_type ) { case 'line_item': case 'product': $classname = "{$items_namespace}POItemProduct"; break; case 'fee': $classname = "{$items_namespace}POItemFee"; break; case 'shipping': $classname = "{$items_namespace}POItemShipping"; break; case 'tax': $classname = "{$items_namespace}POItemTax"; break; default: $classname = apply_filters( 'atum/purchase_orders/po/get_po_item_classname', $classname, $item_type, $id ); break; } if ( $classname && class_exists( $classname ) ) { try { return new $classname( $id ); } catch ( \Exception $e ) { return FALSE; } } } return FALSE; } /** * Get key for where a certain item type is stored in items prop * * @since 1.2.9 * * @param \WC_Order_Item $item PO item object (product, shipping, fee, tax). * * @return string */ protected function get_items_key( $item ) { $items_namespace = '\\Atum\\PurchaseOrders\\Items\\'; if ( is_a( $item, "{$items_namespace}POItemProduct" ) ) { return 'line_items'; } elseif ( is_a( $item, "{$items_namespace}POItemFee" ) ) { return 'fee_lines'; } elseif ( is_a( $item, "{$items_namespace}POItemShipping" ) ) { return 'shipping_lines'; } elseif ( is_a( $item, "{$items_namespace}POItemTax" ) ) { return 'tax_lines'; } else { return ''; } } /** * This method is the inverse of the get_items_key method * Gets the ATUM Order item's class given its key * * @since 1.2.9 * * @param string $items_key The items key. * * @return string */ protected function get_items_class( $items_key ) { switch ( $items_key ) { case 'line_items': return '\\Atum\\PurchaseOrders\\Items\\POItemProduct'; case 'fee_lines': return '\\Atum\\PurchaseOrders\\Items\\POItemFee'; case 'shipping_lines': return '\\Atum\\PurchaseOrders\\Items\\POItemShipping'; case 'tax_lines': return '\\Atum\\PurchaseOrders\\Items\\POItemTax'; default: return ''; } } /** * Get the expected at location date * * @since 1.2.9 * * @return string */ public function get_expected_at_location_date() { return $this->get_meta( '_expected_at_location_date' ); } /** * Use the purchase price for the products added to POs * * @since 1.3.0 * * @param float $price * @param float $qty * @param \WC_Product $product * * @return float|mixed|string */ public function use_purchase_price( $price, $qty, $product ) { // Get the purchase price (if set). /* @noinspection PhpUndefinedMethodInspection */ $price = $product->get_purchase_price(); if ( ! $price ) { return ''; } elseif ( empty( $qty ) ) { return 0.0; } if ( $product->is_taxable() && wc_prices_include_tax() ) { $tax_rates = \WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); $taxes = \WC_Tax::calc_tax( $price * $qty, $tax_rates, TRUE ); $price = \WC_Tax::round( $price * $qty - array_sum( $taxes ) ); } else { $price = $price * $qty; } return $price; } /** * Maybe increase stock Levels * * @since 1.5.0 * * @param int $order_id * @param string $old_status * @param string $new_status * @param PurchaseOrder $order */ public function maybe_decrease_stock_levels( $order_id, $old_status, $new_status, $order ) { if ( PurchaseOrders::FINISHED === $new_status ) { return; } // Any status !== finished is like pending, so reduce stock. if ( $order && PurchaseOrders::FINISHED === $old_status && $old_status !== $new_status && apply_filters( 'atum/purchase_orders/can_reduce_order_stock', TRUE, $order ) ) { $this->change_stock_levels( $order, 'decrease' ); do_action( 'atum/purchase_orders/po/after_decrease_stock_levels', $order ); } } /** * Maybe decrease stock Levels * * @since 1.5.0 * * @param int $order_id * @param PurchaseOrder $order */ public function maybe_increase_stock_levels( $order_id, $order ) { if ( $order && apply_filters( 'atum/purchase_orders/can_restore_order_stock', TRUE, $order ) ) { $this->change_stock_levels( $order, 'increase' ); do_action( 'atum/purchase_orders/po/after_increase_stock_levels', $order ); } } /** * Change product stock from items * * @since 1.5.0 * * @param PurchaseOrder $order * @param string $action */ public function change_stock_levels( $order, $action ) { $atum_order_items = $order->get_items(); if ( ! empty( $atum_order_items ) ) { foreach ( $atum_order_items as $item_id => $atum_order_item ) { $product = $atum_order_item->get_product(); /** * Variable definition * * @var \WC_Product $product */ if ( $product && $product->exists() && $product->managing_stock() ) { $old_stock = $product->get_stock_quantity(); // if stock is null but WC is managing stock. if ( is_null( $old_stock ) ) { $old_stock = 0; wc_update_product_stock( $product, $old_stock ); } $stock_change = apply_filters( 'atum/purchase_orders/po/restore_atum_order_stock_quantity', $atum_order_item->get_quantity(), $item_id ); $new_quantity = wc_update_product_stock( $product, $stock_change, $action ); $old_stock_note = 'increase' === $action ? $new_quantity - $stock_change : $new_quantity + $stock_change; $item_name = $product->get_sku() ? $product->get_sku() : $product->get_id(); $note = sprintf( /* translators: first is the item name, second is the action, third is the old stock and forth is the new stock */ __( 'Item %1$s stock %2$s from %3$s to %4$s.', ATUM_TEXT_DOMAIN ), $item_name, 'increase' === $action ? __( 'increased', ATUM_TEXT_DOMAIN ) : __( 'decreased', ATUM_TEXT_DOMAIN ), $old_stock_note, $new_quantity ); $order->add_note( $note ); $atum_order_item->update_meta_data( '_stock_changed', TRUE ); $atum_order_item->save(); } } } } /** * Recalculate the inbound stock for products within POs, every time a PO is saved. * * @since 1.5.8 * * @param PurchaseOrder $po */ public function after_save( $po ) { $items = $po->get_items(); foreach ( $items as $item ) { /** * Variable definition * * @var LogItemProduct $item */ $product_id = $item->get_variation_id() ?: $item->get_product_id(); $product = Helpers::get_atum_product( $product_id ); if ( is_a( $product, '\WC_Product' ) ) { Helpers::update_order_item_product_data( $product, Globals::get_order_type_table_id( $this->get_type() ) ); } } do_action( 'atum/purchase_orders/after_save', $po, $items ); } }