runTriggers() */ add_action("woocommerce_admin_order_actions_end", [$this, "showPostNLSettings"], 9999); add_action("woocommerce_admin_order_actions_end", [$this, "showOrderActions"], 20); /* * Single order page */ add_action("add_meta_boxes_shop_order", [$this, "add_order_meta_box"]); add_action("woocommerce_admin_order_data_after_shipping_address", [$this, "single_order_shipment_options"]); add_action("wp_ajax_wcpn_save_shipment_options", [$this, "save_shipment_options_ajax"]); add_action("wp_ajax_wcpn_get_shipment_summary_status", [$this, "order_list_ajax_get_shipment_summary"]); add_action("wp_ajax_wcpn_get_shipment_options", [$this, "ajaxGetShipmentOptions"]); // HS code in product shipping options tab add_action("woocommerce_product_options_shipping", [$this, "productHsCodeField"]); add_action("woocommerce_process_product_meta", [$this, "productHsCodeFieldSave"]); // Country of Origin in product shipping options tab add_action("woocommerce_product_options_shipping", [$this, "productCountryOfOriginField"]); add_action("woocommerce_process_product_meta", [$this, "productCountryOfOriginFieldSave"]); // Add barcode in order grid add_filter("manage_edit-shop_order_columns", [$this, "barcode_add_new_order_admin_list_column"], 10, 1); add_action("manage_shop_order_posts_custom_column", [$this, "addBarcodeToOrderColumn"], 10, 2); add_action('woocommerce_payment_complete', [$this, 'automaticExportOrder'], 1000); add_action("init", [$this, "registerDeliveredPostStatus"], 10, 1); add_filter("wc_order_statuses", [$this, "displayDeliveredPostStatus"], 10, 2); add_action('woocommerce_product_after_variable_attributes', array($this, 'variation_hs_code_field'), 10, 3); add_action('woocommerce_save_product_variation', array($this, 'save_variation_hs_code_field'), 10, 2); add_filter('woocommerce_available_variation', array($this, 'load_variation_hs_code_field'), 10, 1); } /** * @param \MyParcelNL\Sdk\src\Adapter\DeliveryOptions\AbstractDeliveryOptionsAdapter $deliveryOptions */ public static function renderPickupLocation(DeliveryOptions $deliveryOptions): void { if (! $deliveryOptions->isPickup()) { return; } $pickup = $deliveryOptions->getPickupLocation(); printf( "
%s:
%s
%s %s
%s %s
", __("Pickup location", "woocommerce-postnl"), $pickup->getLocationName(), $pickup->getStreet(), $pickup->getNumber(), $pickup->getPostalCode(), $pickup->getCity() ); echo "
"; } /** * @param $loop * @param $variationData * @param $variation */ public function variation_hs_code_field($loop, $variationData, $variation) { woocommerce_wp_text_input( array( 'id' => self::META_HS_CODE_VARIATION . "[{$loop}]", 'name' => self::META_HS_CODE_VARIATION . "[{$loop}]", 'value' => get_post_meta($variation->ID, self::META_HS_CODE_VARIATION, true), 'label' => __('HS Code', 'woocommerce'), 'desc_tip' => true, 'description' => __('This HS Code overwrites the parents HS Code.', 'woocommerce'), 'wrapper_class' => 'form-row form-row-full', ) ); } /** * @param $variationId * @param $loop */ public function save_variation_hs_code_field($variationId, $loop) { $hsCodeValue = $_POST[self::META_HS_CODE_VARIATION][$loop]; if (! empty($hsCodeValue)) { update_post_meta($variationId, self::META_HS_CODE_VARIATION, esc_attr($hsCodeValue)); } } /** * @param $variation * * @return mixed */ public function load_variation_hs_code_field($variation) { $variation[self::META_HS_CODE_VARIATION] = get_post_meta($variation['variation_id'], self::META_HS_CODE_VARIATION, true); return $variation; } /** * Add delivered post type to order statuses list */ public function registerDeliveredPostStatus(): void { register_post_status('wc-custom-delivered', [ 'label' => 'Delivered', 'public' => true, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true, 'label_count' => _n_noop('Delivered (%s)', 'Delivered (%s)'), ] ); } /** * @param array $order_statuses * * @return array */ public function displayDeliveredPostStatus(array $order_statuses): array { $new_order_statuses = []; foreach ($order_statuses as $key => $status) { $new_order_statuses[$key] = $status; if ('wc-processing' === $key) { $new_order_statuses['wc-custom-delivered'] = 'Delivered'; } } return $new_order_statuses; } /** * @param $orderId * * @throws ErrorException * @throws \MyParcelNL\Sdk\src\Exception\ApiException * @throws \use MyParcelNL\Sdk\src\Exception\MissingFieldException */ public function automaticExportOrder($orderId): void { (new WCPN_Export())->exportByOrderId($orderId); } /** * @param WC_Order $order * * @throws Exception */ public function showPostNLSettings(WC_Order $order): void { $isAllowedDestination = WCPN_Country_Codes::isAllowedDestination(WCX_Order::get_prop($order, 'shipping_country')); if (! $isAllowedDestination) { return; } $order_id = WCX_Order::get_id($order); $consignments = WCPOST_Admin::get_order_shipments($order); $deliveryOptions = self::getDeliveryOptionsFromOrder($order); $packageType = $deliveryOptions->getPackageType() ?? WCPOST()->export->getPackageTypeFromOrder($order); echo '"; } /** * Get shipment status + Track & Trace link via AJAX * * @throws Exception */ public function order_list_ajax_get_shipment_summary() { check_ajax_referer(WCPOST::NONCE_ACTION, 'security'); include('views/html-order-shipment-summary.php'); die(); } /** * Add export option to bulk action drop down menu. * * @param array $actions * * @return array * @since WordPress 4.7.0 */ public function addBulkActions(array $actions): array { $actions = array_merge( $actions, [ self::BULK_ACTION_EXPORT => __("PostNL: Prepare shipment", "woocommerce-postnl"), self::BULK_ACTION_PRINT => __("PostNL: Print", "woocommerce-postnl"), self::BULK_ACTION_EXPORT_PRINT => __("PostNL: Prepare & print shipment", "woocommerce-postnl"), ] ); self::renderSpinner('bulkAction'); return $actions; } /** * Add export option to bulk action drop down menu * Using Javascript until WordPress core fixes: http://core.trac.wordpress.org/ticket/16031 * * Used pre WordPress 4.7.0 * * @access public * @return void */ public function bulk_actions() { global $post_type; $bulk_actions = [ self::BULK_ACTION_EXPORT => __("PostNL: Prepare shipment", "woocommerce-postnl"), self::BULK_ACTION_PRINT => __("PostNL: Print", "woocommerce-postnl"), self::BULK_ACTION_EXPORT_PRINT => __("PostNL: Prepare & print shipment", "woocommerce-postnl"), ]; if ('shop_order' == $post_type) { ?> setting_collection->isEnabled(WCPOST_Settings::SETTING_ASK_FOR_PRINT_POSITION)) { return; } $field = [ "name" => "offset", "class" => ["wcpn__d--inline-block"], "input_class" => ["wcpn__offset-dialog__offset"], "type" => "number", "label" => __("Labels to skip", "woocommerce-postnl"), "custom_attributes" => [ "step" => "1", "min" => "0", "max" => "4", "size" => "2", ], ]; $class = new SettingsFieldArguments($field); ?> '; } /** * Get the new html content for the shipment options form based on the passed order id. */ public function ajaxGetShipmentOptions(): void { // Order is used in views/html-order-shipment-options.php $order = wc_get_order((int) $_POST['orderId']); include('views/html-order-shipment-options.php'); die(); } /** * Add print actions to the orders listing * * @param $order * * @throws Exception */ public function showOrderActions($order): void { if (empty($order)) { return; } $shipping_country = WCX_Order::get_prop($order, 'shipping_country'); if (! WCPN_Country_Codes::isAllowedDestination($shipping_country)) { return; } $order_id = WCX_Order::get_id($order); $baseUrl = "admin-ajax.php?action=" . WCPN_Export::EXPORT; $addShipments = WCPN_Export::ADD_SHIPMENTS; $getLabels = WCPN_Export::GET_LABELS; $listing_actions = [ $addShipments => [ "url" => admin_url("$baseUrl&request=$addShipments&order_ids=$order_id"), "img" => WCPOST()->plugin_url() . "/assets/img/postnl-up.png", "alt" => __("Export to PostNL", "woocommerce-postnl"), ], $getLabels => [ "url" => admin_url("$baseUrl&request=$getLabels&order_ids=$order_id"), "img" => WCPOST()->plugin_url() . "/assets/img/postnl-pdf.png", "alt" => __("Print PostNL label", "woocommerce-postnl"), ], ]; $consignments = WCPOST_Admin::get_order_shipments($order); if (empty($consignments)) { unset($listing_actions[$getLabels]); } $display = WCPOST()->setting_collection->getByName(WCPOST_Settings::SETTING_DOWNLOAD_DISPLAY) === 'display'; $attributes = []; if ($display) { $attributes["target"] = "_blank"; } foreach ($listing_actions as $request => $data) { self::renderAction( $data['url'], $data['alt'], $data["img"], $attributes ); } } /** * @param WC_Order $order * @param bool $exclude_concepts * * @return array */ public static function get_order_shipments(WC_Order $order, bool $exclude_concepts = false): array { $consignments = WCX_Order::get_meta($order, self::META_SHIPMENTS); // fallback to legacy consignment data (v1.X) if (empty($consignments)) { if ($consignment_id = WCX_Order::get_meta($order, self::META_CONSIGNMENT_ID)) { $consignments = [ [ "shipment_id" => $consignment_id, "track_trace" => WCX_Order::get_meta($order, self::META_TRACK_TRACE), ], ]; } elseif ($legacy_consignments = WCX_Order::get_meta($order, self::META_CONSIGNMENTS)) { $consignments = []; foreach ($legacy_consignments as $consignment) { if (isset($consignment["consignment_id"])) { $consignments[] = [ "shipment_id" => $consignment["consignment_id"], "track_trace" => $consignment["track_trace"], ]; } } } } if (empty($consignments) || ! is_array($consignments)) { return []; } /** * Filter out concepts. */ if ($exclude_concepts) { $consignments = array_filter($consignments, function($consignment) { return isset($consignment["track_trace"]); } ); } return $consignments; } /** * On saving shipment options from the bulk options form. * * @throws Exception * @see admin/views/html-order-shipment-options.php */ public function save_shipment_options_ajax(): void { parse_str($_POST["form_data"], $form_data); foreach ($form_data[self::SHIPMENT_OPTIONS_FORM_NAME] as $order_id => $data) { $order = WCX::get_order($order_id); $shippingCountry = $order->get_shipping_country(); $data = self::removeDisallowedDeliveryOptions($data, $shippingCountry); $deliveryOptions = self::getDeliveryOptionsFromOrder($order, $data); error_log( sprintf( "Saving delivery options for order %d\n%s", $order_id, json_encode( $deliveryOptions->toArray(), JSON_PRETTY_PRINT ) ) ); WCX_Order::update_meta_data( $order, self::META_DELIVERY_OPTIONS, $deliveryOptions ); // Save extra options WCX_Order::update_meta_data( $order, self::META_SHIPMENT_OPTIONS_EXTRA, $data["extra_options"] ); } die(); } /** * Add the meta box on the single order page */ public function add_order_meta_box(): void { add_meta_box( "postnl", __("PostNL", "woocommerce-postnl"), [$this, "createMetaBox"], "shop_order", "side", "default" ); } /** * Callback: Create the meta box content on the single order page * * @throws Exception */ public function createMetaBox(): void { global $post_id; // get order $order = WCX::get_order($post_id); if (! $order) { return; } $order_id = WCX_Order::get_id($order); $shipping_country = WCX_Order::get_prop($order, 'shipping_country'); if (! WCPN_Country_Codes::isAllowedDestination($shipping_country)) { return; } $class = version_compare(WOOCOMMERCE_VERSION, '3.3.0', '>=') ? "single_wc_actions" : "single_order_actions"; // show buttons and check if WooCommerce > 3.3.0 is used and select the correct function and class echo "
"; $this->showOrderActions($order); echo '
'; $downloadDisplay = WCPOST()->setting_collection->getByName(WCPOST_Settings::SETTING_DOWNLOAD_DISPLAY) === 'display'; $consignments = WCPOST_Admin::get_order_shipments($order); // show shipments if available if (empty($consignments)) { return; } include('views/html-order-track-trace-table.php'); } /** * @param $order * * @throws Exception */ public function single_order_shipment_options(WC_Order $order) { $shipping_country = WCX_Order::get_prop($order, "shipping_country"); if (! WCPN_Country_Codes::isAllowedDestination($shipping_country)) { return; } $this->showPostNLSettings($order); } /** * @param \WC_Order $order * * @throws \Exception */ public function showDeliveryDateForOrder(WC_Order $order): void { $deliveryOptions = self::getDeliveryOptionsFromOrder($order); $this->printDeliveryDate($deliveryOptions); } /** * @param $order_id * @param $track_trace * * @return string|void * @throws Exception */ public static function getTrackTraceUrl($order_id, $track_trace) { if (empty($order_id)) { return; } $order = WCX::get_order($order_id); $country = WCX_Order::get_prop($order, 'shipping_country'); $postcode = preg_replace('/\s+/', '', WCX_Order::get_prop($order, 'shipping_postcode')); // set url for NL or foreign orders if ($country === 'NL') { $deliveryOptions = self::getDeliveryOptionsFromOrder($order); // use billing postcode for pickup/pakjegemak if ($deliveryOptions->isPickup()) { $postcode = preg_replace('/\s+/', '', WCX_Order::get_prop($order, 'billing_postcode')); } $trackTraceUrl = sprintf( 'https://jouw.postnl.nl/track-and-trace/%s/%s/%s', $track_trace, $country, $postcode ); } else { $trackTraceUrl = sprintf( 'https://www.internationalparceltracking.com/Main.aspx#/track/%s/%s/%s', $track_trace, $country, $postcode ); } return $trackTraceUrl; } public function productHsCodeField() { echo '
'; woocommerce_wp_text_input( [ 'id' => self::META_HS_CODE, 'label' => __('HS Code', 'woocommerce-postnl'), 'description' => sprintf( __('HS Codes are used for PostNL world shipments, you can find the appropriate code on the %ssite of the Dutch Customs%s.', 'woocommerce-postnl' ), '', '' ), ] ); echo '
'; } public function productHsCodeFieldSave($post_id) { // check if hs code is passed and not an array (=variation hs code) if (isset($_POST[self::META_HS_CODE]) && ! is_array($_POST[self::META_HS_CODE])) { $product = wc_get_product($post_id); $hs_code = $_POST[self::META_HS_CODE]; if (! empty($hs_code)) { WCX_Product::update_meta_data($product, self::META_HS_CODE, esc_attr($hs_code)); } else { if (isset($_POST[self::META_HS_CODE]) && empty($hs_code)) { WCX_Product::delete_meta_data($product, self::META_HS_CODE); } } } } public function productCountryOfOriginField() { echo '
'; woocommerce_wp_text_input( [ 'id' => self::META_COUNTRY_OF_ORIGIN, 'label' => __('Country of Origin', 'woocommerce-postnl'), 'type' => 'select', 'options' => (new WC_Countries())->get_countries(), 'description' => sprintf( __('Country of origin is required for world shipments. Defaults to shop base.') ), ] ); echo '
'; } public function productCountryOfOriginFieldSave($postId) { if (isset($_POST[self::META_COUNTRY_OF_ORIGIN]) && ! is_array($_POST[self::META_COUNTRY_OF_ORIGIN])) { $product = wc_get_product($postId); $countryOfOrigin = $_POST[self::META_COUNTRY_OF_ORIGIN]; if (! empty($countryOfOrigin)) { WCX_Product::update_meta_data($product, self::META_COUNTRY_OF_ORIGIN, esc_attr($countryOfOrigin)); return; } if (isset($_POST[self::META_COUNTRY_OF_ORIGIN]) && empty($countryOfOrigin)) { WCX_Product::delete_meta_data($product, self::META_COUNTRY_OF_ORIGIN); } } return; } /** * @snippet Add Column to Orders Table (e.g. Barcode) - WooCommerce * * @param $columns * * @return mixed */ public function barcode_add_new_order_admin_list_column($columns) { // I want to display Barcode column just after the date column return array_slice($columns, 0, 6, true) + ['barcode' => 'Barcode'] + array_slice($columns, 6, null, true); } /** * @param $column * * @throws Exception */ public function addBarcodeToOrderColumn($column) { global $post; if ("barcode" === $column) { $this->renderBarcodes(WCX::get_order($post->ID)); } } /** * @param WC_Order $order * * @return void * @throws Exception */ public function renderBarcodes(WC_Order $order): void { $shipments = WCPOST_Admin::get_order_shipments($order, true); if (empty($shipments)) { echo __("No label has been created yet.", "woocommerce-postnl"); return; } echo '
'; foreach ($shipments as $shipment_id => $shipment) { if (empty($shipment["track_trace"])) { echo __("Concept created but not printed.", "woocommerce-postnl"); } else { printf( '%2$s
', WCPOST_Admin::getTrackTraceUrl($order, $shipment["track_trace"]), $shipment["track_trace"] ); } } echo "
"; } /** * Get DeliveryOptions object from the given order's meta data. Uses legacy delivery options if found, if that * data is invalid it falls back to defaults. * * @param WC_Order $order * @param array $inputData * * @return DeliveryOptions * @throws \Exception * @see \WCPN_Checkout::save_delivery_options */ public static function getDeliveryOptionsFromOrder(WC_Order $order, array $inputData = []): DeliveryOptions { $meta = WCX_Order::get_meta($order, self::META_DELIVERY_OPTIONS) ?: null; // $meta is a json string, create an instance if (! empty($meta) && ! $meta instanceof DeliveryOptions) { if (is_string($meta)) { $meta = json_decode(stripslashes($meta), true); } $meta["carrier"] = WCPN_Data::DEFAULT_CARRIER; try { // create new instance from known json $meta = DeliveryOptionsAdapterFactory::create((array) $meta); } catch (BadMethodCallException $e) { // create new instance from unknown json data $meta = new WCPN_DeliveryOptionsFromOrderAdapter(null, (array) $meta); } } // Create or update immutable adapter from order with a instanceof DeliveryOptionsAdapter if (empty($meta) || ! empty($inputData)) { $meta = new WCPN_DeliveryOptionsFromOrderAdapter($meta, $inputData); } return $meta; } /** * Output the delivery date if there is a date and the show delivery day setting is enabled. * * @param DeliveryOptions $deliveryOptions * * @throws Exception */ private function printDeliveryDate(DeliveryOptions $deliveryOptions): void { $showDeliveryDay = WCPOST()->setting_collection->isEnabled(WCPOST_Settings::SETTING_SHOW_DELIVERY_DAY); if ($deliveryOptions->getDate() && $showDeliveryDay) { printf( '
%s
%s, %s
', __("PostNL shipment:", "woocommerce-postnl"), WCPN_Data::getDeliveryTypesHuman()[$deliveryOptions->getDeliveryType()], wc_format_datetime(new WC_DateTime($deliveryOptions->getDate()), 'D d-m') ); } } /** * Output a spinner. * * @param string $state * @param array $args */ public static function renderSpinner(string $state = "", array $args = []): void { $spinners = [ "loading" => get_site_url() . "/wp-admin/images/spinner.gif", "success" => get_site_url() . "/wp-admin/images/yes.png", "failed" => get_site_url() . "/wp-admin/images/no.png", ]; $arguments = []; $args["class"][] = "wcpn__spinner"; if ($state) { $args["class"][] = "wcpn__spinner--$state"; } foreach ($args as $arg => $value) { if (is_array($value)) { $value = implode(" ", $value); } $arguments[] = "$arg=\"$value\""; } $attributes = implode(" ", $arguments); echo ""; foreach ($spinners as $spinnerState => $icon) { printf( '%1$s', $spinnerState, $icon, $state === $spinnerState ? "block" : "none" ); } echo ''; } /** * @param string $url * @param string $alt * @param string $icon * @param array $rawAttributes */ public static function renderAction(string $url, string $alt, string $icon, array $rawAttributes = []): void { printf( ' %2$s', wp_nonce_url($url, WCPOST::NONCE_ACTION), $alt, $icon, wc_implode_html_attributes($rawAttributes) ); self::renderSpinner(); echo ""; } /** * @param array $shipment * @param int $order_id * * @throws Exception */ public static function renderTrackTraceLink(array $shipment, int $order_id): void { $track_trace = $shipment["track_trace"] ?? null; if ($track_trace) { $track_trace_url = WCPOST_Admin::getTrackTraceUrl($order_id, $track_trace); $track_trace_link = sprintf( '%s', $track_trace_url, $track_trace ); } elseif (isset($shipment["shipment"]) && isset($shipment["shipment"]["options"])) { $package_type = WCPN_Export::getPackageTypeHuman($shipment["shipment"]["options"]["package_type"]); $track_trace_link = "($package_type)"; } else { $track_trace_link = __("(Unknown)", "woocommerce-postnl"); } echo $track_trace_link; } /** * @param array $shipment * @param int $order_id */ public static function renderStatus(array $shipment, int $order_id): void { echo $shipment["status"] ?? "–"; if (self::shipmentIsStatus($shipment, self::ORDER_STATUS_DELIVERED_AT_RECIPIENT) || self::shipmentIsStatus($shipment, self::ORDER_STATUS_DELIVERED_READY_FOR_PICKUP) || self::shipmentIsStatus($shipment, self::ORDER_STATUS_DELIVERED_PACKAGE_PICKED_UP) ) { $order = WCX::get_order($order_id); // This will be addressed in MY-24881 // $order->update_status('wc-custom-delivered'); } } /** * @param array $shipment * @param int $status * * @return bool */ public static function shipmentIsStatus(array $shipment, int $status): bool { return strstr($shipment['status'], (new WCPN_Export())->getShipmentStatusName($status)); } /** * Remove options that aren't allowed and return the edited array. * * @param array $data * @param string $country * * @return mixed */ private static function removeDisallowedDeliveryOptions(array $data, string $country): array { $data['package_type'] = $data['package_type'] ?? AbstractConsignment::DEFAULT_PACKAGE_TYPE_NAME; $isHomeCountry = WCPN_Data::isHomeCountry($country); $isEuCountry = WCPN_Country_Codes::isEuCountry($country); if (! $isHomeCountry) { $data['package_type'] = AbstractConsignment::DEFAULT_PACKAGE_TYPE_NAME; } $isPackage = AbstractConsignment::PACKAGE_TYPE_PACKAGE_NAME === $data['package_type']; if (! $isHomeCountry || ! $isPackage) { $data['shipment_options']['age_check'] = false; $data['shipment_options']['return_shipment'] = false; $data['shipment_options']['insured'] = false; $data['shipment_options']['insured_amount'] = 0; $data['extra_options']['collo_amount'] = 1; } return $data; } } return new WCPOST_Admin();