├── src ├── js │ ├── writepanel.js │ ├── utils.js │ └── booking-form.js └── css │ └── frontend.scss ├── woocommerce-accommodation-bookings.php ├── includes ├── class-wc-product-accommodation-booking-resource.php ├── class-wc-accommodation-booking-cart-manager.php ├── class-wc-accommodation-dependencies.php ├── admin │ ├── views │ │ ├── html-accommodation-booking-rates.php │ │ ├── html-accommodation-booking-data.php │ │ ├── html-accommodation-booking-rates-fields.php │ │ ├── html-accommodation-booking-availability-fields.php │ │ └── html-accommodation-booking-availability.php │ ├── class-wc-accommodation-booking-admin-panels.php │ ├── class-wc-accommodation-booking-admin-product-settings.php │ └── class-wc-accommodation-booking-rest-and-admin.php ├── class-wc-accommodation-booking-product-tabs.php ├── class-wc-accommodation-booking-order-manager.php ├── integrations │ └── class-wc-accommodation-booking-addons.php ├── class-wc-accommodation-booking.php ├── class-wc-accommodation-bookings-plugin.php ├── class-wc-product-accommodation-booking.php └── class-wc-accommodation-booking-date-picker.php ├── readme.txt └── changelog.txt /src/js/writepanel.js: -------------------------------------------------------------------------------- 1 | import jQuery from 'jquery'; 2 | 3 | jQuery(function ($) { 4 | $('#rates_rows').sortable({ 5 | items: 'tr', 6 | cursor: 'move', 7 | axis: 'y', 8 | handle: '.sort', 9 | scrollSensitivity: 40, 10 | forcePlaceholderSize: true, 11 | helper: 'clone', 12 | opacity: 0.65, 13 | placeholder: 'wc-metabox-sortable-placeholder', 14 | start(event, ui) { 15 | ui.item.css('background-color', '#f6f6f6'); 16 | }, 17 | stop(event, ui) { 18 | ui.item.removeAttr('style'); 19 | }, 20 | }); 21 | 22 | function wc_accommodation_bookings_trigger_change_events() { 23 | $('#_wc_accommodation_booking_has_restricted_days').trigger('change'); 24 | } 25 | 26 | $('#_wc_accommodation_booking_has_restricted_days').on( 27 | 'change', 28 | function () { 29 | if ($(this).is(':checked')) { 30 | $('.booking-day-restriction').show(); 31 | } else { 32 | $('.booking-day-restriction').hide(); 33 | } 34 | } 35 | ); 36 | 37 | wc_accommodation_bookings_trigger_change_events(); 38 | }); 39 | -------------------------------------------------------------------------------- /woocommerce-accommodation-bookings.php: -------------------------------------------------------------------------------- 1 | run(); 35 | -------------------------------------------------------------------------------- /src/css/frontend.scss: -------------------------------------------------------------------------------- 1 | .product-type-accommodation-booking { 2 | .wc-bookings-date-picker { 3 | position: relative; 4 | padding-top: 55px; 5 | 6 | &::after { 7 | content: attr(data-content); 8 | position: absolute; 9 | background: #fff; 10 | margin: 10px 20px; 11 | font-size: 13px; 12 | left: 0; 13 | right: 0; 14 | top: 0; 15 | text-align: center; 16 | height: 35px; 17 | line-height: 35px; 18 | } 19 | 20 | .picker.hasDatepicker { 21 | margin: 0; 22 | position: static !important; 23 | } 24 | } 25 | } 26 | 27 | .wc-bookings-date-picker { 28 | fieldset { 29 | position: relative; 30 | } 31 | 32 | &:not([data-selected-date-type="start"]) .ui-datepicker td.fully_booked_start_days:not(.selection-end-date), 33 | &[data-selected-date-type="start"] .ui-datepicker td.fully_booked_end_days:not(.selection-start-date) { 34 | opacity: 0.35; 35 | } 36 | 37 | &:not([data-selected-date-type="start"]) .ui-datepicker td.fully_booked_start_days:not(.selection-end-date), 38 | &[data-selected-date-type="start"] .ui-datepicker td.fully_booked_end_days:not(.selection-start-date), 39 | .ui-datepicker td.fully_booked { 40 | span, a { 41 | background-color: #c0392b !important; 42 | background-image: none !important; 43 | border-color: rgba(0, 0, 0, 0.1) !important; 44 | color: #fff !important; 45 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); 46 | pointer-events: none; 47 | } 48 | } 49 | 50 | .ui-datepicker { 51 | td.bookable-range.ui-state-disabled { 52 | opacity: 1; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /includes/class-wc-product-accommodation-booking-resource.php: -------------------------------------------------------------------------------- 1 | get_parent_id() && ( $parent = wc_get_product( $this->get_parent_id() ) ) && $parent->is_type( 'accommodation-booking' ) ) { 25 | $costs = $parent->get_resource_base_costs(); 26 | $cost = isset( $costs[ $this->get_id() ] ) ? $costs[ $this->get_id() ] : ''; 27 | } else { 28 | $cost = ''; 29 | } 30 | 31 | return (float) $cost; 32 | } 33 | 34 | /** 35 | * Return the block cost (set at parent level). 36 | * @version 1.0.9 37 | * @since 1.0.9 38 | * 39 | * @return float 40 | */ 41 | public function get_block_cost() { 42 | if ( $this->get_parent_id() && ( $parent = wc_get_product( $this->get_parent_id() ) ) && $parent->is_type( 'accommodation-booking' ) ) { 43 | $costs = $parent->get_resource_block_costs(); 44 | $cost = isset( $costs[ $this->get_id() ] ) ? $costs[ $this->get_id() ] : ''; 45 | } else { 46 | $cost = ''; 47 | } 48 | return (float) $cost; 49 | } 50 | } 51 | 52 | endif; 53 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | import jQuery from 'jquery'; 2 | /** 3 | * Should return whether the product is a accommodation booking or not. 4 | * 5 | * @since 1.1.40 6 | * 7 | * @param {Object} $booking_form 8 | * 9 | * @return {boolean} Returns true if the product is a accommodation booking product type. 10 | */ 11 | export function is_product_type_accommodation_booking($booking_form) { 12 | return ( 13 | $booking_form.closest('.product.product-type-accommodation-booking') 14 | .length > 0 15 | ); 16 | } 17 | 18 | /** 19 | * Should return booking form jQuery selector. 20 | * 21 | * @since 1.1.40 22 | * 23 | * @param {Object} $field 24 | * 25 | * @return {Object} Return booking form jQuery element. 26 | */ 27 | export function get_booking_form($field) { 28 | // Convert to jQuery selector. 29 | $field = get_jquery_element($field); 30 | 31 | return $field.closest('form'); 32 | } 33 | 34 | /** 35 | * Should return jQuery selector element. 36 | * 37 | * @since 1.1.40 38 | * 39 | * @param {Object} $field jQuery selector. 40 | * 41 | * @return {jQuery} Return jQuery element. 42 | */ 43 | export function get_jquery_element($field) { 44 | return jQuery($field); 45 | } 46 | 47 | /** 48 | * Should return date type. 49 | * 50 | * @since 1.1.40 51 | * @param {jQuery} $date_picker Date picker jQuery element. 52 | * @return {string} date_type Selected date type. Value can be 'start' or 'end'. 53 | */ 54 | export function get_selected_date_type($date_picker) { 55 | const next_date_type = $date_picker.data('start_or_end_date'); 56 | let date_type = null; 57 | 58 | switch (next_date_type) { 59 | case 'end': 60 | date_type = 'start'; 61 | break; 62 | 63 | case 'start': 64 | default: 65 | date_type = 'end'; 66 | } 67 | 68 | return date_type; 69 | } 70 | -------------------------------------------------------------------------------- /includes/class-wc-accommodation-booking-cart-manager.php: -------------------------------------------------------------------------------- 1 | get_type() && ! empty( $other_data ) ) { 36 | $check_in = WC_Product_Accommodation_Booking::get_check_times( 'in', $cart_item['product_id'] ); 37 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $cart_item['product_id'] ); 38 | $end_date = date_i18n( get_option( 'date_format'), $cart_item['booking']['_end_date'] ); 39 | 40 | if ( ! empty( $check_in ) ) { 41 | $other_data[] = array( 42 | 'name' => __( 'Check-in', 'woocommerce-accommodation-bookings' ), 43 | 'value' => esc_html( $cart_item['booking']['date'] . __( ' at ', 'woocommerce-accommodation-bookings' ) . date_i18n( get_option( 'time_format' ), strtotime( "Today " . $check_in ) ) ), 44 | 'display' => '' 45 | ); 46 | } 47 | 48 | if ( ! empty( $check_out ) ) { 49 | $other_data[] = array( 50 | 'name' => __( 'Check-out', 'woocommerce-accommodation-bookings' ), 51 | 'value' => esc_html( $end_date . __( ' at ', 'woocommerce-accommodation-bookings' ) . date_i18n( get_option( 'time_format' ), strtotime( "Today " . $check_out ) ) ), 52 | 'display' => '', 53 | ); 54 | } 55 | 56 | // This data was added by Woo Booking plugin, so we need to override. 57 | foreach ( $other_data as $key => $data ) { 58 | if ( 'Booking Date' === $data['name'] ) { 59 | $other_data[ $key ]['value'] = date_i18n( wc_bookings_date_format(), get_post_datetime( $cart_item['booking']['_booking_id'] ) ); 60 | break; 61 | } 62 | } 63 | } 64 | 65 | return $other_data; 66 | } 67 | } 68 | 69 | new WC_Accommodation_Booking_Cart_Manager(); 70 | -------------------------------------------------------------------------------- /includes/class-wc-accommodation-dependencies.php: -------------------------------------------------------------------------------- 1 | =' ) ) { 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | /** 70 | * Check dependencies. 71 | * 72 | * @throws Exception Show exception on dependencies failure. 73 | */ 74 | public static function check_dependencies() { 75 | if ( ! self::$active_plugins ) { 76 | self::init(); 77 | } 78 | 79 | if ( ! self::is_bookings_installed() ) { 80 | throw new Exception( esc_html__( 'Accommodation Bookings requires Bookings plugin activated.', 'woocommerce-accommodation-bookings' ) ); 81 | } 82 | 83 | if ( ! self::is_bookings_above_or_equal_to_version( '1.9.0' ) ) { 84 | throw new Exception( esc_html__( 'Accommodation Bookings requires Bookings version 1.9+.', 'woocommerce-accommodation-bookings' ) ); 85 | } 86 | 87 | if ( ! version_compare( PHP_VERSION, '7.4', '>=' ) ) { 88 | throw new Exception( esc_html__( 'Accommodation Bookings requires PHP version 7.4 or above.', 'woocommerce-accommodation-bookings' ) ); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /includes/admin/views/html-accommodation-booking-rates.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | '_wc_accommodation_booking_base_cost', 'label' => __( 'Standard room rate', 'woocommerce-accommodation-bookings' ), 'description' => __( 'Standard cost for booking the room.', 'woocommerce-accommodation-bookings' ), 'value' => get_post_meta( $post_id, '_wc_booking_base_cost', true ), 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => array( 4 | 'min' => '', 5 | 'step' => '0.01' 6 | ) ) ); ?> 7 | 8 | 9 | '_wc_accommodation_booking_display_cost', 'label' => __( 'Display cost', 'woocommerce-accommodation-bookings' ), 'description' => __( 'The cost is displayed to the user on the frontend. Leave blank to have it calculated for you. If a booking has varying costs, this will be prefixed with the word "from:".', 'woocommerce-accommodation-bookings' ), 'value' => get_post_meta( $post_id, '_wc_display_cost', true ), 'type' => 'number', 'desc_tip' => true, 'custom_attributes' => array( 10 | 'min' => '', 11 | 'step' => '0.01' 12 | ) ) ); ?> 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 | 42 | 50 | 51 |
  [?] [?] 
32 | 38 |
52 |
53 | 54 |
55 |
-------------------------------------------------------------------------------- /includes/class-wc-accommodation-booking-product-tabs.php: -------------------------------------------------------------------------------- 1 | product_id ); 31 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $this->product_id ); 32 | 33 | if ( empty( $check_in ) ) { 34 | return false; 35 | } 36 | 37 | if ( empty( $check_out ) ) { 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | /** 45 | * Adds our time tab to the list of tabs, if the product is an accommodation product and 46 | * the admin filled out the time setting fields. 47 | * 48 | * @param array $tabs List of WooCommerce product tabs 49 | * @return array $tabs 50 | */ 51 | public function add_time_tab( $tabs = array() ) { 52 | global $post; 53 | 54 | if ( ! is_object( $post ) ) { 55 | return $tabs; 56 | } 57 | 58 | $product = wc_get_product( $post->ID ); 59 | 60 | if ( 'accommodation-booking' !== $product->get_type() ) { 61 | return $tabs; 62 | } 63 | 64 | // Set the product ID. 65 | $this->product_id = $post->ID; 66 | 67 | if ( ! $this->are_time_fields_filled_out() ) { 68 | return $tabs; 69 | } 70 | 71 | $title = apply_filters( 'woocommerce_accommodation_booking_time_tab_title', esc_html__( 'Arriving/leaving', 'woocommerce-accommodation-bookings' ) ); 72 | $tabs['accommodation_booking_time'] = array( 73 | 'title' => $title, 74 | 'priority' => 10, 75 | 'callback' => array( $this, 'add_time_tab_content' ), 76 | ); 77 | 78 | return $tabs; 79 | } 80 | 81 | /** 82 | * The actual content for our time tab. 83 | */ 84 | public function add_time_tab_content() { 85 | if ( ! $this->are_time_fields_filled_out() ) { 86 | return; 87 | } 88 | $check_in = WC_Product_Accommodation_Booking::get_check_times( 'in', $this->product_id ); 89 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $this->product_id ); 90 | ?> 91 |

92 | 96 | get_type() ) { 26 | return; 27 | } 28 | 29 | $booking_date_key = __( 'Booking Date', 'woocommerce-accommodation-bookings' ); 30 | $booking_date = ! empty( $item[ $booking_date_key ] ) ? $item[ $booking_date_key ] : null; 31 | 32 | if ( ! $booking_date ) { 33 | return; 34 | } 35 | 36 | $check_in = WC_Product_Accommodation_Booking::get_check_times( 'in', $item['product_id'] ); 37 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $item['product_id'] ); 38 | ?> 39 |

40 | 41 | get_end_date_timestamp( $booking_date, $duration ); 50 | if ( ! $end_date_ts ) { 51 | return; 52 | } 53 | ?> 54 | 55 |
56 | 57 | 58 | 66 |

67 | add( new DateInterval( 'P' . $duration . 'D' ) ); 83 | 84 | return $datetime->getTimestamp(); 85 | } 86 | 87 | /** 88 | * Complete virtual booking orders, 89 | * 90 | * Hooked into a filter that changes the status for bookings. 91 | * 92 | * @param $order_status 93 | * @param $order_id 94 | * @return string 95 | */ 96 | public function complete_order( $order_status, $order_id ) { 97 | $order = wc_get_order( $order_id ); 98 | if ( 'processing' === $order_status 99 | && $order->has_status( array( 'on-hold', 'pending', 'failed' ) ) ) { 100 | $virtual_booking_order = false; 101 | 102 | if ( count( $order->get_items() ) < 1 ) { 103 | return $order_status; 104 | } 105 | 106 | foreach ( $order->get_items() as $item ) { 107 | if ( $item->is_type( 'line_item' ) ) { 108 | $product = $item->get_product(); 109 | $virtual_booking_order = $product && $product->is_virtual() && $product->is_type( 'accommodation-booking' ); 110 | } 111 | if ( ! $virtual_booking_order ) { 112 | break; 113 | } 114 | } 115 | // virtual order, mark as completed 116 | if ( $virtual_booking_order ) { 117 | return 'completed'; 118 | } 119 | } 120 | 121 | // non-virtual order, return original status 122 | return $order_status; 123 | } 124 | 125 | } 126 | 127 | new WC_Accommodation_Booking_Order_Manager; 128 | -------------------------------------------------------------------------------- /includes/admin/class-wc-accommodation-booking-admin-panels.php: -------------------------------------------------------------------------------- 1 | ID; 49 | include( 'views/html-accommodation-booking-data.php' ); 50 | } 51 | 52 | /** 53 | * Loads any CSS or JS necessary for the admin 54 | */ 55 | public function admin_styles_and_scripts() { 56 | 57 | $screen = get_current_screen(); 58 | 59 | // only load it on products 60 | if ( 'product' === $screen->id ) { 61 | wp_enqueue_script( 'wc_accommodation_bookings_writepanel_js', WC_ACCOMMODATION_BOOKINGS_PLUGIN_URL . '/build/js/admin/writepanel.js', array( 'jquery' ), WC_ACCOMMODATION_BOOKINGS_VERSION, true ); 62 | } 63 | } 64 | 65 | /** 66 | * Loads our panels related to accommodation bookings 67 | * @version 1.0.11 68 | */ 69 | public function panels() { 70 | global $post, $bookable_product; 71 | $post_id = $post->ID; 72 | 73 | /** 74 | * Day restrictions added to Bookings 1.10.7 75 | * @todo Remove version compare ~Aug 2018 76 | */ 77 | if ( version_compare( WC_BOOKINGS_VERSION, '1.10.7', '>=' ) ) { 78 | 79 | if ( empty( $bookable_product ) || $bookable_product->get_id() !== $post->ID ) { 80 | $bookable_product = new WC_Product_Booking( $post->ID ); 81 | } 82 | 83 | $restricted_meta = $bookable_product->get_restricted_days(); 84 | 85 | for ( $i=0; $i < 7; $i++) { 86 | 87 | if ( $restricted_meta && in_array( $i, $restricted_meta ) ) { 88 | $restricted_days[ $i ] = $i; 89 | } else { 90 | $restricted_days[ $i ] = false; 91 | } 92 | } 93 | } 94 | 95 | include( 'views/html-accommodation-booking-rates.php' ); 96 | include( 'views/html-accommodation-booking-availability.php' ); 97 | } 98 | 99 | 100 | /** 101 | * Hides the "virtal" option for accommodations 102 | * @param array $options 103 | * @return array 104 | */ 105 | public function product_type_options( $options ) { 106 | $options['virtual']['wrapper_class'] .= ' show_if_accommodation-booking'; 107 | $options['wc_booking_has_resources']['wrapper_class'] .= ' show_if_accommodation-booking'; 108 | $options['wc_booking_has_persons']['wrapper_class'] .= ' show_if_accommodation-booking'; 109 | return $options; 110 | } 111 | 112 | /** 113 | * Add tab entries definition 114 | * 115 | * @param array $tabs List of tabs. 116 | * @return array 117 | */ 118 | public function add_tabs( $tabs ) { 119 | $tabs['accommodation_bookings_pricing'] = array( 120 | 'label' => __( 'Rates', 'woocommerce-accommodation-bookings' ), 121 | 'target' => 'accommodation_bookings_rates', 122 | 'class' => array( 'show_if_accommodation-booking', 'accommodation_bookings_tab', 'bookings_pricing_tab', 'advanced_options' ), 123 | 'priority' => 80, 124 | ); 125 | $tabs['accommodation_bookings_availability'] = array( 126 | 'label' => __( 'Availability', 'woocommerce-accommodation-bookings' ), 127 | 'target' => 'accommodation_bookings_availability', 128 | 'class' => array( 'show_if_accommodation-booking', 'accommodation_bookings_tab', 'bookings_availability_tab', 'advanced_options' ), 129 | 'priority' => 80, 130 | ); 131 | 132 | return $tabs; 133 | } 134 | } 135 | 136 | new WC_Accommodation_Booking_Admin_Panels(); 137 | -------------------------------------------------------------------------------- /includes/admin/views/html-accommodation-booking-data.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | '_wc_accommodation_booking_min_duration', 11 | 'label' => __( 'Minimum number of nights allowed in a booking', 'woocommerce-accommodation-bookings' ), 12 | 'description' => __( 'The minimum allowed duration the user can stay.', 'woocommerce-accommodation-bookings' ), 13 | 'value' => ( empty( $min_duration ) ? 1 : $min_duration ), 14 | 'desc_tip' => true, 15 | 'type' => 'number', 16 | 'custom_attributes' => array( 17 | 'min' => '', 18 | 'step' => '1', 19 | ), 20 | ) 21 | ); 22 | 23 | woocommerce_wp_text_input( 24 | array( 25 | 'id' => '_wc_accommodation_booking_max_duration', 26 | 'label' => __( 'Maximum number of nights allowed in a booking', 'woocommerce-accommodation-bookings' ), 27 | 'description' => __( 'The maximum allowed duration the user can stay.', 'woocommerce-accommodation-bookings' ), 28 | 'value' => ( empty( $max_duration ) ? 7 : $max_duration ), 29 | 'desc_tip' => true, 30 | 'type' => 'number', 31 | 'custom_attributes' => array( 32 | 'min' => '1', 33 | 'step' => '1', 34 | ), 35 | ) 36 | ); 37 | 38 | woocommerce_wp_select( 39 | array( 40 | 'id' => '_wc_accommodation_booking_calendar_display_mode', 41 | 'label' => __( 'Calendar display mode', 'woocommerce-accommodation-bookings' ), 42 | 'description' => __( 'Choose how the calendar is displayed on the booking form.', 'woocommerce-accommodation-bookings' ), 43 | 'value' => get_post_meta( $post_id, '_wc_booking_calendar_display_mode', true ), 44 | 'options' => array( 45 | '' => __( 'Display calendar on click', 'woocommerce-accommodation-bookings' ), 46 | 'always_visible' => __( 'Calendar always visible', 'woocommerce-accommodation-bookings' ), 47 | ), 48 | 'desc_tip' => true, 49 | 'class' => 'select', 50 | ) 51 | ); 52 | 53 | woocommerce_wp_checkbox( 54 | array( 55 | 'id' => '_wc_accommodation_booking_requires_confirmation', 56 | 'label' => __( 'Requires confirmation?', 'woocommerce-accommodation-bookings' ), 57 | 'description' => __( 'Check this box if the booking requires admin approval/confirmation. Payment will not be taken during checkout.', 'woocommerce-accommodation-bookings' ), 58 | 'value' => wc_bookings_string_to_bool( get_post_meta( $post_id, '_wc_booking_requires_confirmation', true ) ) ? 'yes' : 'no', 59 | ) 60 | ); 61 | 62 | woocommerce_wp_checkbox( 63 | array( 64 | 'id' => '_wc_accommodation_booking_user_can_cancel', 65 | 'label' => __( 'Can be cancelled?', 'woocommerce-accommodation-bookings' ), 66 | 'description' => __( 'Check this box if the booking can be cancelled by the customer after it has been purchased. A refund will not be sent automatically.', 'woocommerce-accommodation-bookings' ), 67 | 'value' => wc_bookings_string_to_bool( get_post_meta( $post_id, '_wc_booking_user_can_cancel', true ) ) ? 'yes' : 'no', 68 | ) 69 | ); 70 | 71 | $cancel_limit = max( absint( get_post_meta( $post_id, '_wc_booking_cancel_limit', true ) ), 1 ); 72 | $cancel_limit_unit = get_post_meta( $post_id, '_wc_booking_cancel_limit_unit', true ); 73 | ?> 74 |

75 | 76 | 77 | 83 | 84 |

85 | 86 | 89 |
90 | -------------------------------------------------------------------------------- /src/js/booking-form.js: -------------------------------------------------------------------------------- 1 | // External dependencies. 2 | import { __ } from '@wordpress/i18n'; 3 | import jQuery from 'jquery'; 4 | 5 | // Internal dependencies. 6 | import { 7 | get_booking_form, 8 | get_jquery_element, 9 | get_selected_date_type, 10 | is_product_type_accommodation_booking, 11 | } from './utils'; 12 | 13 | // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars 14 | (function ($) { 15 | const HookApi = window.wc_bookings.hooks; 16 | 17 | // Filter the date element attributes. 18 | HookApi.addFilter( 19 | 'wc_bookings_date_picker_get_day_attributes', 20 | 'wc_accommodation_booking/booking_form', 21 | ( 22 | attributes, 23 | { booking_data, custom_data, date_picker, resource_id, date } 24 | ) => { 25 | const $form = get_booking_form(date_picker); 26 | const year = date.getFullYear(); 27 | const month = date.getMonth() + 1; 28 | const day = date.getDate(); 29 | const ymdIndex = `${year}-${month}-${day}`; 30 | 31 | // Exit if product is not accommodation booking. 32 | if (!is_product_type_accommodation_booking($form)) { 33 | return attributes; 34 | } 35 | 36 | if ( 37 | booking_data.fully_booked_start_days && 38 | booking_data.fully_booked_start_days[ymdIndex] && 39 | (custom_data.resources_assignment === 'automatic' || 40 | booking_data.fully_booked_start_days[ymdIndex][0] || 41 | booking_data.fully_booked_start_days[ymdIndex][resource_id]) 42 | ) { 43 | attributes.class.push('fully_booked_start_days'); 44 | } 45 | 46 | if ( 47 | booking_data.fully_booked_end_days && 48 | booking_data.fully_booked_end_days[ymdIndex] && 49 | (custom_data.resources_assignment === 'automatic' || 50 | booking_data.fully_booked_end_days[ymdIndex][0] || 51 | booking_data.fully_booked_end_days[ymdIndex][resource_id]) 52 | ) { 53 | attributes.class.push('fully_booked_end_days'); 54 | } 55 | 56 | if (attributes.class.indexOf('fully_booked_start_days') > -1) { 57 | attributes.title = __( 58 | 'Available for check-out only.', 59 | 'woocommerce-accommodation-bookings' 60 | ); 61 | } else if (attributes.class.indexOf('fully_booked_end_days') > -1) { 62 | attributes.title = __( 63 | 'Available for check-in only.', 64 | 'woocommerce-accommodation-bookings' 65 | ); 66 | } 67 | 68 | return attributes; 69 | } 70 | ); 71 | 72 | // Make the days disable and unselectable according to the selection. 73 | HookApi.addAction( 74 | 'wc_bookings_date_picker_refreshed', 75 | 'wc_accommodation_booking/booking_form', 76 | ({ date_picker }) => { 77 | const $form = get_booking_form(date_picker); 78 | 79 | // Exit if product is not accommodation booking. 80 | if (!is_product_type_accommodation_booking($form)) { 81 | return; 82 | } 83 | 84 | $form 85 | .find('fieldset') 86 | .attr( 87 | 'data-content', 88 | __('Select check-in', 'woocommerce-accommodation-bookings') 89 | ); 90 | $form 91 | .find('.fully_booked_start_days') 92 | .addClass('ui-datepicker-unselectable ui-state-disabled'); 93 | $form 94 | .find('.fully_booked_end_days') 95 | .removeClass('ui-datepicker-unselectable ui-state-disabled'); 96 | } 97 | ); 98 | 99 | // Add attribute to field set when date selected start date. 100 | HookApi.addAction( 101 | 'wc_bookings_date_selected', 102 | 'wc_accommodation_booking/booking_form', 103 | ({ fieldset, date_picker }) => { 104 | const $fieldset = get_jquery_element(fieldset); 105 | const $date_picker = get_jquery_element(date_picker); 106 | const date_type = get_selected_date_type($date_picker); 107 | const $form = get_booking_form(fieldset); 108 | let data_content = ''; 109 | 110 | // Exit if product is not accommodation booking. 111 | if (!is_product_type_accommodation_booking($form)) { 112 | return; 113 | } 114 | 115 | $fieldset.attr('data-selected-date-type', date_type); 116 | 117 | switch (date_type) { 118 | case 'end': 119 | data_content = __( 120 | 'Selected! Re-select to change your check-in date.', 121 | 'woocommerce-accommodation-bookings' 122 | ); 123 | break; 124 | 125 | case 'start': 126 | default: 127 | data_content = __( 128 | 'Select check-out', 129 | 'woocommerce-accommodation-bookings' 130 | ); 131 | } 132 | 133 | $fieldset.attr('data-content', data_content); 134 | } 135 | ); 136 | 137 | // Toogle accomadated date as per selected date. 138 | HookApi.addAction( 139 | 'wc_bookings_pre_calculte_booking_cost', 140 | 'wc_accommodation_booking/booking_form', 141 | ({ form, date_picker }) => { 142 | const $date_picker = get_jquery_element(date_picker); 143 | const $form = get_jquery_element(form); 144 | const date_type = get_selected_date_type($date_picker); 145 | 146 | // Exit if product is not accommodation booking. 147 | if (!is_product_type_accommodation_booking($form)) { 148 | return; 149 | } 150 | 151 | switch (date_type) { 152 | case 'end': 153 | $form 154 | .find('.fully_booked_start_days') 155 | .addClass( 156 | 'ui-datepicker-unselectable ui-state-disabled' 157 | ); 158 | $form 159 | .find('.fully_booked_end_days') 160 | .removeClass( 161 | 'ui-datepicker-unselectable ui-state-disabled' 162 | ); 163 | break; 164 | 165 | case 'start': 166 | default: 167 | $form 168 | .find('.fully_booked_start_days') 169 | .removeClass( 170 | 'ui-datepicker-unselectable ui-state-disabled' 171 | ); 172 | $form 173 | .find('.fully_booked_end_days') 174 | .addClass( 175 | 'ui-datepicker-unselectable ui-state-disabled' 176 | ); 177 | } 178 | } 179 | ); 180 | })(jQuery); 181 | -------------------------------------------------------------------------------- /includes/integrations/class-wc-accommodation-booking-addons.php: -------------------------------------------------------------------------------- 1 | is_type( 'accommodation-booking' ) && 29 | ( defined( 'WC_PRODUCT_ADDONS_VERSION' ) && version_compare( WC_PRODUCT_ADDONS_VERSION, '3.0', '<' ) ) 30 | ) { 31 | $show_grand_total = false; 32 | } 33 | 34 | return $show_grand_total; 35 | } 36 | 37 | /** 38 | * Show same options as bookings integration now. 39 | * Only difference is the product type we are targeting. 40 | */ 41 | public function addon_options( $post, $addon, $loop ) { 42 | $css_classes = 'show_if_accommodation-booking'; 43 | 44 | if ( is_object( $post ) ) { 45 | $product = wc_get_product( $post->ID ); 46 | if ( 'accommodation-booking' !== $product->get_type() ) { 47 | $css_classes .= ' hide_initial_booking_addon_options'; 48 | } 49 | } else { 50 | $css_classes .= ' hide_initial_booking_addon_options'; 51 | } 52 | 53 | if ( defined( 'WC_PRODUCT_ADDONS_VERSION' ) && version_compare( WC_PRODUCT_ADDONS_VERSION, '3.0', '<' ) ) { 54 | ?> 55 | 56 | 57 | 58 | /> 59 | 60 | 61 | 62 | /> 63 | 64 | 65 | 68 |
69 |
70 | 74 |
75 | 76 |
77 | 81 |
82 |
83 | is_type( 'accommodation-booking' ) ) { 97 | return false; 98 | } 99 | return $bool; 100 | } 101 | 102 | /** 103 | * Change addon price based on settings 104 | * @return float 105 | */ 106 | public function addon_price( $cart_item_data, $addon, $product_id, $post_data ) { 107 | 108 | // Option `wc_accommodation_booking_block_qty_multiplier` is deprecated and identical to booking's `wc_booking_block_qty_multiplier`. 109 | foreach ( $cart_item_data as $key => $data ) { 110 | if ( ! empty( $addon['wc_accommodation_booking_block_qty_multiplier'] ) ) { 111 | // Intentionally using a different key as these mean the same thing, and we will not need to duplicate code here. 112 | $cart_item_data[ $key ]['wc_booking_block_qty_multiplier'] = 1; 113 | } 114 | } 115 | 116 | return $cart_item_data; 117 | } 118 | } 119 | 120 | new WC_Accommodation_Booking_Addons(); 121 | -------------------------------------------------------------------------------- /includes/admin/views/html-accommodation-booking-rates-fields.php: -------------------------------------------------------------------------------- 1 | __( 'January', 'woocommerce-accommodation-bookings' ), 6 | '2' => __( 'February', 'woocommerce-accommodation-bookings' ), 7 | '3' => __( 'March', 'woocommerce-accommodation-bookings' ), 8 | '4' => __( 'April', 'woocommerce-accommodation-bookings' ), 9 | '5' => __( 'May', 'woocommerce-accommodation-bookings' ), 10 | '6' => __( 'June', 'woocommerce-accommodation-bookings' ), 11 | '7' => __( 'July', 'woocommerce-accommodation-bookings' ), 12 | '8' => __( 'August', 'woocommerce-accommodation-bookings' ), 13 | '9' => __( 'September', 'woocommerce-accommodation-bookings' ), 14 | '10' => __( 'October', 'woocommerce-accommodation-bookings' ), 15 | '11' => __( 'November', 'woocommerce-accommodation-bookings' ), 16 | '12' => __( 'December', 'woocommerce-accommodation-bookings' ) 17 | ); 18 | 19 | $intervals['days'] = array( 20 | '1' => __( 'Monday', 'woocommerce-accommodation-bookings' ), 21 | '2' => __( 'Tuesday', 'woocommerce-accommodation-bookings' ), 22 | '3' => __( 'Wednesday', 'woocommerce-accommodation-bookings' ), 23 | '4' => __( 'Thursday', 'woocommerce-accommodation-bookings' ), 24 | '5' => __( 'Friday', 'woocommerce-accommodation-bookings' ), 25 | '6' => __( 'Saturday', 'woocommerce-accommodation-bookings' ), 26 | '7' => __( 'Sunday', 'woocommerce-accommodation-bookings' ) 27 | ); 28 | 29 | for ( $i = 1; $i <= 53; $i ++ ) { 30 | /* translators: %s: week number */ 31 | $intervals['weeks'][ $i ] = sprintf( __( 'Week %s', 'woocommerce-accommodation-bookings' ), $i ); 32 | } 33 | 34 | if ( ! isset( $rate['type'] ) ) { 35 | $rate['type'] = 'custom'; 36 | } 37 | ?> 38 | 39 | 40 |   41 | 42 |
43 | 49 |
50 | 51 | 52 |
53 | 58 |
59 |
60 | 65 |
66 |
67 | 72 |
73 |
74 | 75 |
76 | 77 | 78 |
79 | 84 |
85 |
86 | 91 |
92 |
93 | 98 |
99 |
100 | 101 |
102 | 103 |
104 | 105 |
106 | 107 | 108 | 122 | 123 | 124 |   125 | 126 | -------------------------------------------------------------------------------- /includes/admin/views/html-accommodation-booking-availability-fields.php: -------------------------------------------------------------------------------- 1 | __( 'January', 'woocommerce-accommodation-bookings' ), 6 | '2' => __( 'February', 'woocommerce-accommodation-bookings' ), 7 | '3' => __( 'March', 'woocommerce-accommodation-bookings' ), 8 | '4' => __( 'April', 'woocommerce-accommodation-bookings' ), 9 | '5' => __( 'May', 'woocommerce-accommodation-bookings' ), 10 | '6' => __( 'June', 'woocommerce-accommodation-bookings' ), 11 | '7' => __( 'July', 'woocommerce-accommodation-bookings' ), 12 | '8' => __( 'August', 'woocommerce-accommodation-bookings' ), 13 | '9' => __( 'September', 'woocommerce-accommodation-bookings' ), 14 | '10' => __( 'October', 'woocommerce-accommodation-bookings' ), 15 | '11' => __( 'November', 'woocommerce-accommodation-bookings' ), 16 | '12' => __( 'December', 'woocommerce-accommodation-bookings' ) 17 | ); 18 | 19 | $intervals['days'] = array( 20 | '1' => __( 'Monday', 'woocommerce-accommodation-bookings' ), 21 | '2' => __( 'Tuesday', 'woocommerce-accommodation-bookings' ), 22 | '3' => __( 'Wednesday', 'woocommerce-accommodation-bookings' ), 23 | '4' => __( 'Thursday', 'woocommerce-accommodation-bookings' ), 24 | '5' => __( 'Friday', 'woocommerce-accommodation-bookings' ), 25 | '6' => __( 'Saturday', 'woocommerce-accommodation-bookings' ), 26 | '7' => __( 'Sunday', 'woocommerce-accommodation-bookings' ) 27 | ); 28 | 29 | for ( $i = 1; $i <= 53; $i ++ ) { 30 | /* translators: %s: week number */ 31 | $intervals['weeks'][ $i ] = sprintf( __( 'Week %s', 'woocommerce-accommodation-bookings' ), $i ); 32 | } 33 | 34 | if ( ! isset( $availability['type'] ) ) { 35 | $availability['type'] = 'custom'; 36 | } 37 | 38 | if ( ! isset( $availability['priority'] ) ) { 39 | $availability['priority'] = 10; 40 | } 41 | ?> 42 | 43 |   44 | 45 |
46 | 52 |
53 | 54 | 55 |
56 | 61 |
62 |
63 | 68 |
69 |
70 | 75 |
76 |
77 | 78 |
79 | 80 | 81 |
82 | 87 |
88 |
89 | 94 |
95 |
96 | 101 |
102 |
103 | 104 |
105 | 106 | 107 |
108 | 112 |
113 | 114 | 115 |
116 | 117 |
118 | 119 |   120 | 121 | -------------------------------------------------------------------------------- /includes/admin/class-wc-accommodation-booking-admin-product-settings.php: -------------------------------------------------------------------------------- 1 | Settings > Products > Accommodations 7 | */ 8 | class WC_Accommodation_Booking_Admin_Product_Settings extends WC_Settings_API { 9 | /** 10 | * The single instance of the class. 11 | * 12 | * @var $_instance 13 | * @since 1.13.0 14 | */ 15 | protected static $_instance = null; 16 | 17 | /** 18 | * Name for nonce to update times settings. 19 | * 20 | * @since 1.13.0 21 | * @var string self::NONCE_NAME 22 | */ 23 | const NONCE_NAME = 'bookings_times_settings_nonce'; 24 | 25 | /** 26 | * Action name for nonce to update times settings. 27 | * 28 | * @since 1.13.0 29 | * @var string self::NONCE_ACTION 30 | */ 31 | const NONCE_ACTION = 'submit_bookings_times_settings'; 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @since 1.13.0 37 | */ 38 | public function __construct() { 39 | $this->plugin_id = "woocommerce_accommodation_bookings_"; 40 | $this->id = "times"; 41 | 42 | $this->maybe_migrate(); 43 | 44 | // Initialize settings and form data. 45 | $this->init_times_settings(); 46 | 47 | add_action( 'admin_init', array( $this, 'maybe_save_settings' ) ); 48 | add_filter( 'woocommerce_bookings_settings_page', array( $this, 'add_accommodation_settings' ) ); 49 | } 50 | 51 | /** 52 | * Maybe migrate data from old format to new one. 53 | */ 54 | public function maybe_migrate() { 55 | $check_in = get_option( 'woocommerce_accommodation_bookings_check_in' ); 56 | 57 | if ( $check_in ) { 58 | delete_option( 'woocommerce_accommodation_bookings_check_in' ); 59 | } 60 | 61 | $check_out = get_option( 'woocommerce_accommodation_bookings_check_out' ); 62 | 63 | if ( $check_out ) { 64 | delete_option( 'woocommerce_accommodation_bookings_check_out' ); 65 | } 66 | 67 | if ( $check_in || $check_out ) { 68 | update_option( $this->plugin_id . $this->id . '_settings', array( 69 | 'check_in' => $check_in, 70 | 'check_out' => $check_out, 71 | ) ); 72 | } 73 | } 74 | 75 | /** 76 | * Initialize settings by using Bookings filter. 77 | * 78 | * @param array $tabs_metadata Tabs metadata. 79 | * 80 | * @return array Modified metadata that includes Accommodation. 81 | */ 82 | public function add_accommodation_settings( $tabs_metadata ) { 83 | $tabs_metadata['accommodation'] = array( 84 | 'name' => __( 'Accommodation', 'woocommerce-accommodation-bookings' ), 85 | 'href' => admin_url( 'edit.php?post_type=wc_booking&page=wc_bookings_settings&tab=accommodation' ), 86 | 'capability' => 'manage_options', 87 | 'generate_html' => 'WC_Accommodation_Booking_Admin_Product_Settings::generate_form_html', 88 | ); 89 | 90 | return $tabs_metadata; 91 | } 92 | 93 | /** 94 | * Initialize settings and form data. 95 | * 96 | * @since 1.13.0 97 | * @return void 98 | */ 99 | public function init_times_settings() { 100 | // Load the form fields. 101 | $this->init_form_fields(); 102 | 103 | // Load the settings. 104 | $this->init_settings(); 105 | } 106 | 107 | /** 108 | * Update settings values from form. 109 | * 110 | * @since 1.13.0 111 | * @return void 112 | */ 113 | public function maybe_save_settings() { 114 | if ( isset( $_POST['Submit'] ) 115 | && isset( $_POST[ self::NONCE_NAME ] ) 116 | && wp_verify_nonce( wc_clean( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) { 117 | $this->process_admin_options(); 118 | 119 | echo '

' . esc_html__( 'Settings saved', 'woocommerce-accommodation-bookings' ) . '

'; 120 | 121 | do_action( 'wc_bookings_times_settings_on_save', $this ); 122 | } 123 | } 124 | 125 | /** 126 | * Defines settings fields. 127 | * 128 | * @since 1.13.0 129 | * @return void 130 | */ 131 | public function init_form_fields() { 132 | global $wp_locale; 133 | 134 | $this->form_fields = array( 135 | 'check_in' => array( 136 | 'title' => __( 'Check-in time', 'woocommerce-accommodation-bookings' ), 137 | 'desc' => __( 'Check-in time for reservations.', 'woocommerce-accommodation-bookings' ), 138 | 'default' => '14:00', 139 | 'type' => 'accommodation_time', 140 | ), 141 | 'check_out' => array( 142 | 'title' => __( 'Check-out time', 'woocommerce-accommodation-bookings' ), 143 | 'desc' => __( 'Check-out time for reservations.', 'woocommerce-accommodation-bookings' ), 144 | 'default' => '12:00', 145 | 'type' => 'accommodation_time', 146 | ), 147 | ); 148 | } 149 | 150 | /** 151 | * Returns true if settings exist in database. 152 | * 153 | * @since 1.13.0 154 | * @return bool 155 | */ 156 | public static function exists_in_db() { 157 | $maybe_settings = get_option( self::instance()->get_option_key(), null ); 158 | return is_array( $maybe_settings ); 159 | } 160 | 161 | /** 162 | * Generates full HTML form for the instance settings. 163 | * 164 | * @since 1.13.0 165 | * @return void 166 | */ 167 | public static function generate_form_html() { 168 | ?> 169 |
170 | admin_options(); ?> 171 |

172 | 173 | 174 |

175 |
176 | get_option( $key ); 204 | } 205 | 206 | /** 207 | * Outputs a time selector input box 208 | * @param array $value The "setting" info from init_form_fields. 209 | */ 210 | public function generate_accommodation_time_html( $key, $value ) { 211 | $field_key = $this->get_field_key( $key ); 212 | $type = $value['type']; 213 | $option_value = get_option( $this->plugin_id . $this->id . '_settings' ); 214 | $option_value = isset( $option_value[ $key ] ) ? $option_value[ $key ] : ''; 215 | ob_start(); 216 | 217 | ?> 218 | 219 | 220 | 221 | 222 | 228 | 229 | 2 |
3 | '_wc_accommodation_booking_qty', 'label' => __( 'Number of rooms available', 'woocommerce-accommodation-bookings' ), 'description' => __( 'The maximum number of rooms available.', 'woocommerce-accommodation-bookings' ), 'value' => max( absint( get_post_meta( $post_id, '_wc_booking_qty', true ) ), 1 ), 'desc_tip' => true, 'type' => 'number', 'custom_attributes' => array( 4 | 'min' => '', 5 | 'step' => '1' 6 | ) ) ); ?> 7 | 11 |

12 | 13 | 14 | 19 |

20 | 28 |

29 | 30 | 31 | 36 |

37 | 38 | =' ) ) : 44 | 45 | woocommerce_wp_checkbox( 46 | array( 47 | 'id' => '_wc_accommodation_booking_has_restricted_days', 48 | 'value' => $bookable_product->has_restricted_days( 'edit' ) ? 'yes' : 'no', 49 | 'label' => __( 'Restrict selectable days?', 'woocommerce-accommodation-bookings' ), 50 | 'description' => __( 'Restrict the days of the week that are able to be selected on the calendar; this will not affect your availability.', 'woocommerce-accommodation-bookings' ), 51 | ) 52 | ); 53 | ?> 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 74 | 78 | 81 | 82 | 83 | 84 |
  75 | 76 | > 77 |  
85 |
86 | 87 | 88 | 89 |
90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 115 | 116 | 117 | 118 | 126 | 127 |
  [?] [?] 
107 | 113 | 114 |
128 |
129 |
130 | 131 | 132 | -------------------------------------------------------------------------------- /includes/class-wc-accommodation-booking.php: -------------------------------------------------------------------------------- 1 | get_product(); 47 | 48 | if ( ! is_a( $product, 'WC_Product_Accommodation_Booking' ) ) { 49 | // Return original, unmodified value 50 | return date( 'Ymd\THis', $old_ts ); 51 | } 52 | 53 | return $value; 54 | } 55 | 56 | /** 57 | * Apply only one rate modifier to any given accomodation block. 58 | * 59 | * Rate rules for accomodation bookings define the absolute accomodation 60 | * rate for a block, so it does not make sense to apply multiple concurrent 61 | * rates. This hook will cause the rate calculation loop to exit after the 62 | * first applicable rate rule modifies the block cost. 63 | * 64 | * @since 1.1.12 65 | * 66 | * @param bool $enable_overlapping_rates 67 | * @param WC_Product_Booking $product 68 | * @return array 69 | */ 70 | public function disable_overlapping_rates( $enable_overlapping_rates, $product ) { 71 | if ( ! is_a( $product, 'WC_Product_Accommodation_Booking' ) ) { 72 | return $enable_overlapping_rates; 73 | } 74 | return false; 75 | } 76 | 77 | /** 78 | * Register data stores for bookings. 79 | * 80 | * @param array $data_stores 81 | * @return array 82 | */ 83 | public function register_data_stores( $data_stores = array() ) { 84 | if ( isset( $data_stores['product-booking'] ) ) { 85 | $data_stores['product-accommodation-booking'] = $data_stores['product-booking']; 86 | } 87 | return $data_stores; 88 | } 89 | 90 | /** 91 | * Hooks into woocommerce_bookings_product_types and adds our new type 92 | * @param array $types Array of WooCommerce Bookings Product Types 93 | */ 94 | public function add_product_type( $types ) { 95 | $types[] = 'accommodation-booking'; 96 | return $types; 97 | } 98 | 99 | /** 100 | * Hooks into woocommerce_bookings_product_type_rest_check and verifies that product is a correct bookings type 101 | */ 102 | public function validate_rest_product_type( $is_product_valid, $product ) { 103 | return $is_product_valid || 'accommodation-booking' === $product->get_type(); 104 | } 105 | 106 | public function add_checkin_time_to_booking_start_time( $date, $booking ) { 107 | $product = wc_get_product( $booking->product_id ); 108 | if ( empty( $product ) || 'accommodation-booking' !== $product->get_type() ) { 109 | return $date; 110 | } 111 | 112 | $date_format = apply_filters( 'woocommerce_bookings_date_format', wc_date_format() ); 113 | $time_format = apply_filters( 'woocommerce_bookings_time_format', ', ' . wc_time_format() ); 114 | 115 | return date_i18n( $date_format, $booking->start ) . date_i18n( $time_format, $booking->start ); 116 | } 117 | 118 | public function add_checkout_time_to_booking_end_time( $date, $booking ) { 119 | $product = wc_get_product( $booking->product_id ); 120 | if ( empty( $product ) || 'accommodation-booking' !== $product->get_type() ) { 121 | return $date; 122 | } 123 | 124 | $date_format = apply_filters( 'woocommerce_bookings_date_format', wc_date_format() ); 125 | $time_format = apply_filters( 'woocommerce_bookings_time_format', ', ' . wc_time_format() ); 126 | 127 | return date_i18n( $date_format, $booking->end ) . date_i18n( $time_format, $booking->end ); 128 | } 129 | 130 | /** 131 | * Adds 'accommodation-booking' to the list of valid product types/terms 132 | */ 133 | public function add_accommodation_to_booking_product_terms( $terms ) { 134 | $terms[] = 'accommodation-booking'; 135 | return $terms; 136 | } 137 | 138 | /** 139 | * Adds 'accommodation-booking' to `get_booking_products` so 'accommodation-booking' 140 | * products appear in the dropdown. 141 | * 142 | * @param array $args Current query args. 143 | * 144 | * @return array Query args 145 | */ 146 | public function add_accommodation_to_booking_products_args( $args ) { 147 | // If no tax query, return the args as it is. 148 | if ( empty( $args['tax_query'] ) ) { 149 | return $args; 150 | } 151 | 152 | /* 153 | * Booking V3 has updated tax query for the product_type taxonomy and it now includes all registered product types. 154 | * So we need to check whether the accommodation booking product type is already present before adding it again. 155 | */ 156 | $has_accommodation_booking_in_tax_query = array_filter( 157 | $args['tax_query'], 158 | function ( $filter ) { 159 | if ( ! is_array( $filter ) || 'product_type' !== $filter['taxonomy'] ) { 160 | return false; 161 | } 162 | 163 | $terms = $filter['terms'] ?? ''; 164 | 165 | return is_array( $terms ) 166 | ? in_array( 'accommodation-booking', $terms, true ) 167 | : 'accommodation-booking' === $terms; 168 | } 169 | ); 170 | 171 | // Accommodation booking product type is already there in the tax query, return the args as it is. 172 | if ( ! empty( $has_accommodation_booking_in_tax_query ) ) { 173 | return $args; 174 | } 175 | 176 | foreach ( $args['tax_query'] as $index => $filter ) { 177 | if ( ! is_array( $filter ) || 'product_type' !== $filter['taxonomy'] ) { 178 | continue; 179 | } 180 | 181 | $terms = isset( $args['tax_query'][ $index ]['terms'] ) ? $args['tax_query'][ $index ]['terms'] : array( 'booking' ); 182 | if ( ! is_array( $terms ) ) { 183 | $terms = array( $terms ); 184 | } 185 | $terms[] = 'accommodation-booking'; 186 | 187 | $args['tax_query'][ $index ]['terms'] = $terms; 188 | } 189 | 190 | return $args; 191 | } 192 | 193 | /** 194 | * Update start and end time based on check in/out time. 195 | * 196 | * Since duration of accommodation product is one night, only date is respected 197 | * and check-in/out times are not counted. 198 | * 199 | * @since 1.0.6 200 | * 201 | * @param int $booking_id Booking ID 202 | */ 203 | public function update_start_end_time( $booking_id ) { 204 | $product_id = get_post_meta( $booking_id, '_booking_product_id', true ); 205 | $product = wc_get_product( $product_id ); 206 | if ( ! is_a( $product, 'WC_Product_Accommodation_Booking' ) ) { 207 | return; 208 | } 209 | 210 | $check_in = WC_Product_Accommodation_Booking::get_check_times( 'in', $product_id ); 211 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $product_id ); 212 | 213 | $start = get_post_meta( $booking_id, '_booking_start', true ); 214 | $end = get_post_meta( $booking_id, '_booking_end', true ); 215 | 216 | update_post_meta( $booking_id, '_booking_start', $this->_get_updated_timestamp_time( $start, $check_in ) ); 217 | update_post_meta( $booking_id, '_booking_end', $this->_get_updated_timestamp_time( $end, $check_out ) ); 218 | } 219 | 220 | /** 221 | * Updates resource duration display string when duration unit is 'night'. 222 | * 223 | * @since 1.1.15 224 | * 225 | * @param string $duration_display Duration to display. 226 | * @param object $product The product we are working with. 227 | * 228 | * @return string $duration_display Duration to display. 229 | */ 230 | public function filter_resource_duration_display_string( $duration_display, $product ) { 231 | if ( ! is_a( $product, 'WC_Product_Accommodation_Booking' ) || ( 'night' !== $product->get_duration_unit() ) ) { 232 | return $duration_display; 233 | } 234 | 235 | return __( 'night', 'woocommerce-accommodation-bookings' ); 236 | } 237 | 238 | /** 239 | * Update the time of a given datetime. 240 | * 241 | * @param string $datetime Date time without separator (YmdHis) 242 | * @param string $time Time in `xx:yy` pm/am format 243 | * 244 | * @return Datetime without separtor 245 | */ 246 | protected function _get_updated_timestamp_time( $datetime, $time ) { 247 | if ( empty( $time ) ) { 248 | return $datetime; 249 | } 250 | 251 | $time = str_replace( ':', '', $time ); 252 | $time = str_pad( $time, 6, '0' ); 253 | 254 | return substr( $datetime, 0, 8 ) . $time; 255 | } 256 | 257 | } 258 | 259 | new WC_Accommodation_Booking; 260 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WooCommerce Accommodation Bookings === 2 | Contributors: woocommerce, automattic 3 | Tags: woocommerce, bookings, accommodations 4 | Requires at least: 6.7 5 | Tested up to: 6.9 6 | Stable tag: 1.3.6 7 | License: GNU General Public License v3.0 8 | License URI: http://www.gnu.org/licenses/gpl-3.0.html 9 | 10 | An add-on for WooCommerce Bookings, making it easier to sell hotel rooms, apartments, and spaces to your customers with WooCommerce. 11 | 12 | == Description == 13 | 14 | Accommodation Bookings is a free add-on for WooCommerce and the WooCommerce Bookings extension that makes it easier for you to rent out your space or run a hotel. 15 | 16 | This extension extends Bookings and makes it possible to: 17 | 18 | * Easily configure room rates for specific nights. 19 | * Reservations that span nights instead of days. 20 | * List check-in/check-out information on the product page, cart, and order pages. 21 | 22 | Accommodation Bookings is fully [compatible with WooPayments](https://woocommerce.com/products/woopayments/). 23 | 24 | == Installation == 25 | 26 | 1. Make sure WooCommerce & WooCommerce Bookings are installed. 27 | 1. Download the plugin file to your computer and unzip it. 28 | 2. Using an FTP program, or your hosting control panel, upload the unzipped plugin folder to your WordPress installation’s wp-content/plugins/ directory. 29 | 3. Activate the plugin from the Plugins menu within the WordPress admin. 30 | 31 | Or use the automatic installation wizard through your admin panel, just search for this plugins name. 32 | 33 | == Frequently Asked Questions == 34 | 35 | = Why do my Accommodation Products show higher prices than I defined in the dashboard? = 36 | 37 | If the prices shown on the product do not match the prices defined in the dashboard, the caching mechanism used for pricing calculation is most likely still using old information (e.g. when you updated the prices, or when changing a Bookable product to an Accommodation product). The quickest way to make sure that your prices are correct is to save your existing accommodation product again. The save will update the cache and the price on your site will now reflect what you have defined in your dashboard. 38 | 39 | == Changelog == 40 | 41 | = 1.3.6 - 2025-12-15 = 42 | * Fix - Ensure there is no fatal error on the bookings list page due to recent changes in Bookings V3. 43 | * Fix - JavasScript error reference to jQuery on New Product page. 44 | * Dev - Bump WooCommerce "tested up to" version 10.4. 45 | * Dev - Bump WooCommerce minimum supported version to 10.2. 46 | * Dev - Bump WordPress "tested up to" version 6.9. 47 | * Dev - Update PHP Compat sniffs to a development version covering most of the PHP 8.4 changes. 48 | * Dev - Run the PHP Compat sniffs against the entire production release of the plugin. 49 | * Dev - Updates to our QIT GitHub Action workflow. 50 | 51 | = 1.3.5 - 2025-10-06 = 52 | * Dev - Bump WooCommerce "tested up to" version 10.2. 53 | * Dev - Bump WooCommerce minimum supported version to 10.0. 54 | 55 | = 1.3.4 - 2025-08-11 = 56 | * Dev - Bump WooCommerce "tested up to" version 10.1. 57 | * Dev - Bump WooCommerce minimum supported version to 9.9. 58 | * Dev - Bump WordPress minimum supported version to 6.7. 59 | * Dev - Ensure JavaScript dependencies are set correctly. 60 | * Dev - Update NPM packages to fix security issues. 61 | 62 | = 1.3.3 - 2025-05-27 = 63 | * Dev - Bump WooCommerce "tested up to" version 9.9. 64 | * Dev - Bump WooCommerce minimum supported version to 9.7. 65 | * Dev - Bump WordPress "tested up to" version 6.8. 66 | * Dev - Update all third-party actions our workflows rely on to use versions based on specific commit hashes. 67 | 68 | = 1.3.2 - 2025-03-10 = 69 | * Update - Render uniform WP time picker over default time input element. 70 | * Dev - Bump WooCommerce "tested up to" version 9.7. 71 | * Dev - Bump WooCommerce minimum supported version to 9.5. 72 | * Dev - Bump WordPress minimum supported version to 6.6. 73 | * Dev - Disabled warning checks from WordPress Plugin Check Action. 74 | 75 | = 1.3.1 - 2025-01-20 = 76 | * Dev - Bump WooCommerce "tested up to" version 9.6. 77 | * Dev - Bump WooCommerce minimum supported version to 9.4. 78 | * Dev - Use the `@woocommerce/e2e-utils-playwright` NPM package for E2E tests. 79 | 80 | = 1.3.0 - 2024-12-02 = 81 | * Fix - Ensure sorting by price works as expected. 82 | * Dev - Bump WooCommerce "tested up to" version 9.5. 83 | * Dev - Bump WooCommerce minimum supported version to 9.3. 84 | 85 | = 1.2.10 - 2024-11-18 = 86 | * Dev - Bump WordPress "tested up to" version 6.7. 87 | 88 | = 1.2.9 - 2024-10-28 = 89 | * Dev - Bump WooCommerce "tested up to" version 9.4. 90 | * Dev - Bump WooCommerce minimum supported version to 9.2. 91 | * Dev - Bump WordPress minimum supported version to 6.5. 92 | 93 | = 1.2.8 - 2024-08-28 = 94 | * Fix - Ensure display of checkbox options shows correctly in an Accommodation product. 95 | * Fix - Allow Accommodation product tabs to be ordered properly. 96 | * Dev - Bump WooCommerce "tested up to" version 9.2. 97 | * Dev - Bump WooCommerce minimum supported version to 9.0. 98 | * Dev - Fix QIT E2E tests and add support for a few new test types. 99 | * Dev - Update E2E tests to accommodate the changes in WooCommerce 9.2. 100 | 101 | = 1.2.7 - 2024-07-15 = 102 | * Dev - Bump WooCommerce "tested up to" version 9.0. 103 | * Dev - Bump WooCommerce minimum supported version to 8.8. 104 | * Dev - Bump WordPress "tested up to" version 6.6. 105 | * Dev - Bump WordPress minimum supported version to 6.4. 106 | * Dev - Update NPM packages and node version to v20 to modernize developer experience. 107 | * Dev - Add proper plugin name to the `readme.txt`. Remove Bookings from our required plugins list as it's not hosted on WordPress.org. 108 | * Dev - Exclude the Woo Comment Hook `@since` sniff. 109 | * Fix - Unavailable accommodation days appear available even when booked. 110 | * Fix - Error in Updating Accommodation Product Data via REST API: `Unrecognized Product Type Response`. 111 | 112 | = 1.2.6 - 2024-05-20 = 113 | * Dev - Bump WooCommerce "tested up to" version 8.9. 114 | * Dev - Bump WooCommerce minimum supported version to 8.7. 115 | * Dev - Bump WordPress "tested up to" version 6.5. 116 | * Dev - Bump WordPress minimum supported version to 6.3. 117 | 118 | = 1.2.5 - 2024-02-26 = 119 | * Dev - Apply same code style (call static method) within file. 120 | * Dev - Bump WooCommerce "tested up to" version 8.6. 121 | * Dev - Bump WooCommerce minimum supported version to 8.4. 122 | * Fix - Booking Calendar displays incorrect availability for Accommodation Products. 123 | * Fix - Missing product ID when getting check-in time. 124 | 125 | = 1.2.4 - 2024-02-05 = 126 | * Dev - Bump PHP "tested up to" version 8.3. 127 | * Dev - Bump WooCommerce "tested up to" version 8.5. 128 | * Dev - Bump WooCommerce minimum supported version to 8.3. 129 | * Dev - Bump WordPress minimum supported version to 6.3. 130 | 131 | = 1.2.3 - 2024-01-08 = 132 | * Dev - Declare compatibility with WooCommerce Blocks. 133 | * Dev - Bump PHP minimum supported version to 7.4. 134 | * Dev - Bump WooCommerce "tested up to" version 8.4. 135 | * Dev - Bump WooCommerce minimum supported version to 8.2. 136 | 137 | = 1.2.2 - 2023-12-11 = 138 | * Dev - Add end-to-end tests using Playwright. 139 | * Dev - Update default behavior to use a block-based cart and checkout in E2E tests. 140 | * Dev - Bump WooCommerce "tested up to" version 8.3. 141 | * Dev - Bump WooCommerce minimum supported version to 8.1. 142 | * Dev - Bump WordPress "tested up to" version 6.4. 143 | * Dev - Bump WordPress minimum supported version to 6.2. 144 | 145 | = 1.2.1 - 2023-10-10 = 146 | * Dev - Hard code the paths to the asset data files. 147 | * Dev - Update PHPCS and PHPCompatibility GitHub Actions. 148 | * Fix - Fatal Error when WooCommerce is disabled. 149 | * Tweak - Indicate compatibility with WooPayments extension. 150 | 151 | = 1.2.0 - 2023-09-05 = 152 | * Dev - Bump PHP minimum supported version from 7.0 to 7.3. 153 | * Dev - Bump WooCommerce "tested up to" version from 7.8 to 8.0. 154 | * Dev - Bump WooCommerce minimum supported version from 7.2 to 7.8. 155 | * Dev - Bump WordPress "tested up to" version from 6.2 to 6.3. 156 | 157 | = 1.1.43 - 2023-07-17 = 158 | * Dev - Bump WooCommerce "tested up to" version 7.8. 159 | * Dev - Bump WooCommerce minimum supported version from 6.0 to 7.2. 160 | * Dev - Bump WordPress minimum supported version from 5.6 to 6.1. 161 | 162 | = 1.1.42 - 2023-06-14 = 163 | * Dev - Bump WooCommerce "tested up to" version to 7.6. 164 | * Dev - Bump WooCommerce minimum supported version from 6.0 to 6.8. 165 | * Dev - Bump WordPress "tested up to" version to 6.2. 166 | * Dev - Bump WordPress minimum supported version from 5.6 to 5.8. 167 | * Dev - Added new GitHub Workflow to run Quality Insights Toolkit tests. 168 | 169 | = 1.1.41 - 2023-05-26 = 170 | * Dev - Add product unit filter, `wc_bookings_product_duration_fallback`, to add night unit support. 171 | * Dev - Fix linting errors found by the Quality Insights Toolkit. 172 | 173 | = 1.1.40 - 2023-05-12 = 174 | * Dev - Added a new filter, `woocommerce_accommodation_booking_get_check_times`, to change the check-in/out timings per product. 175 | * Fix - Fully booked days show as partially booked - Day after booking shows partially booked. 176 | 177 | = 1.1.39 - 2023-03-14 = 178 | * Dev - Bump `http-cache-semantics` from 4.1.0 to 4.1.1. 179 | 180 | = 1.1.38 - 2023-03-13 = 181 | * Dev - Bump `http-cache-semantics` from 4.1.0 to 4.1.1. 182 | 183 | = 1.1.37 - 2023-01-27 = 184 | * Dev - Bump `scss-tokenizer` from 0.3.0 to 0.4.3 and `node-sass` from 7.0.1 to 7.0.3. 185 | * Fix - Fatal error when Rate is empty and Range Cost is added. 186 | * Tweak - Bump WooCommerce "tested up to" from 6.7 to 7.3. 187 | * Tweak - Bump WordPress tested up to version from 6.0 to 6.1. 188 | * Tweak - Bump minimum required WordPress version from 4.1 to 5.6. 189 | 190 | = 1.1.36 - 2022-11-24 = 191 | * Update - Bump NPM to v8. 192 | * Update - Bump composer to v2. 193 | * Update - Bump node to v16. 194 | 195 | = 1.1.35 - 2022-11-09 = 196 | * Add - Declare support for High-performance Order Storage ("HPOS"). 197 | * Bump minimist from 1.2.5 to 1.2.6. 198 | * Fix - Showing check-in date on the cart page instead of booking date. 199 | * Tweak - Minimum Version Bumps: WP 5.6, Woo 6.0, & PHP 7.0. 200 | 201 | = 1.1.34 - 2022-11-01 = 202 | * Fixed - PHP 8.0/8.1 Compatibility issue fixed: Critical error when cost in range is empty if Standard room rate is empty as well. 203 | 204 | = 1.1.33 - 2022-08-10 = 205 | * Fix - Build artifact includes node_modules. 206 | 207 | = 1.1.32 - 2022-08-01 = 208 | * Tweak - WC 6.7.0 compatibility. 209 | * Update all npm dependencies. 210 | 211 | = 1.1.31 - 2022-07-06 = 212 | * Fix - Fatal error with PHP 8.0 when Base Cost/Block Cost is empty. 213 | 214 | = 1.1.30 - 2022-06-02 = 215 | * Fix - PHP Warnings on the setting page. 216 | * Tweak - Bump tested up to WordPress version 6.0. 217 | 218 | = 1.1.29 - 2022-06-02 = 219 | * Fix - PHP Warnings on the setting page. 220 | * Tweak - Bump tested up to WordPress version 6.0. 221 | 222 | = 1.1.27 - 2022-06-02 = 223 | * Fix - PHP Warnings on the setting page. 224 | * Tweak - Bump tested up to WordPress version 6.0. 225 | 226 | = 1.1.26 - 2021-11-30 = 227 | * Fix - Bump y18n from 3.2.1 to 3.2.2. 228 | * Tweak - WC 5.9 compatibility. 229 | * Tweak - WP 5.8 compatibility. 230 | 231 | = 1.1.25 - 2021-10-28 = 232 | * Tweak - Change start days settings label to selectable days to be more accurate with functionality. 233 | * Tweak - WC 5.8 compatibility. 234 | * Tweak - WP 5.8 compatibility. 235 | 236 | = 1.1.24 - 2021-05-11 = 237 | * Fix - Added tested up to comment for WordPress compatibility to make it easier to use common tooling. 238 | * Fix - Replace deprecated jQuery 3 methods. 239 | 240 | = 1.1.23 - 2021-02-25 = 241 | * Fix - Dev - Fix: Add casts to float before applying the 'abs' function to potentially empty strings for compatibility with PHP8. 242 | * Tweak - WC 5.0 compatibility. 243 | 244 | = 1.1.22 - 2020-10-27 = 245 | * Tweak - WC 4.6 compatibility. 246 | 247 | = 1.1.21 - 2020-09-29 = 248 | * Fix - Skip formatting dates in ICS output for Accommodation Bookable products. 249 | * Fix - Allow products to be specified as virtual. 250 | * Fix - Use time for transient to skip autoload. 251 | * Tweak - Migrate Settings from Product to Bookings screen. 252 | 253 | = 1.1.20 - 2020-08-25 = 254 | * Fix - Do not round cost values in range types in Rates section. 255 | 256 | = 1.1.19 - 2020-08-19 = 257 | * Tweak - WordPress 5.5 compatibility. 258 | 259 | = 1.1.18 - 2020-07-08 = 260 | * Fix - Existing booking checkout date showed as fully booked and not selectable. 261 | 262 | = 1.1.17 - 2020-06-10 = 263 | * Tweak - WC tested up to 4.2. 264 | 265 | = 1.1.15 - 2020-03-06 = 266 | * Add - Add basic unit tests suite. 267 | * Fix - Fix missing translation for resource duration display. 268 | * Tweak - WP tested up to 5.4. 269 | 270 | [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-accommodation-bookings/master/changelog.txt). 271 | -------------------------------------------------------------------------------- /includes/class-wc-accommodation-bookings-plugin.php: -------------------------------------------------------------------------------- 1 | plugin_file = $plugin_file; 47 | $this->version = $version; 48 | 49 | // Declare compatibility with High-Performance Order Storage. 50 | add_action( 'before_woocommerce_init', array( $this, 'declare_hpos_compatibility' ) ); 51 | } 52 | 53 | /** 54 | * Declare compatibility with High-Performance Order Storage. 55 | * 56 | * @since 1.1.34 57 | */ 58 | public function declare_hpos_compatibility() { 59 | if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { 60 | \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', $this->plugin_file, true ); 61 | } 62 | } 63 | 64 | /** 65 | * Run the plugin. 66 | */ 67 | public function run() { 68 | $this->_define_constants(); 69 | $this->_register_hooks(); 70 | } 71 | 72 | /** 73 | * Handles additional tasks when product is duplicated. 74 | * 75 | * @since 1.1.14 76 | * @param WC_Product $new_product Duplicated product. 77 | * @param WC_Product $product Original product. 78 | * @return void 79 | */ 80 | public function woocommerce_duplicate_product( $new_product, $product ) { 81 | if ( $product->is_type( 'accommodation-booking' ) ) { 82 | // Clone and re-save person types. 83 | foreach ( $product->get_person_types() as $person_type ) { 84 | $dupe_person_type = clone $person_type; 85 | $dupe_person_type->set_id( 0 ); 86 | $dupe_person_type->set_parent_id( $new_product->get_id() ); 87 | $dupe_person_type->save(); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Define plugin's constants. 94 | * 95 | * @return void 96 | */ 97 | // phpcs:ignore 98 | private function _define_constants() { 99 | define( 'WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH', untrailingslashit( plugin_dir_path( $this->plugin_file ) ) . '/includes/' ); 100 | define( 'WC_ACCOMMODATION_BOOKINGS_TEMPLATE_PATH', untrailingslashit( plugin_dir_path( $this->plugin_file ) ) . '/templates/' ); 101 | define( 'WC_ACCOMMODATION_BOOKINGS_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( $this->plugin_file ) ), basename( $this->plugin_file ) ) ) ); 102 | define( 'WC_ACCOMMODATION_BOOKINGS_MAIN_FILE', $this->plugin_file ); 103 | } 104 | 105 | /** 106 | * Register to hooks. 107 | * 108 | * @return void 109 | */ 110 | // phpcs:ignore 111 | private function _register_hooks() { 112 | register_activation_hook( $this->plugin_file, array( $this, 'check_dependencies' ) ); 113 | add_action( 'plugins_loaded', array( $this, 'check_dependencies' ) ); 114 | 115 | if ( is_wp_error( $this->check_dependencies() ) ) { 116 | return; 117 | } 118 | 119 | add_action( 'init', array( $this, 'load_plugin_textdomain' ), 5 ); 120 | add_action( 'plugins_loaded', array( $this, 'includes' ), 20 ); 121 | add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); 122 | add_action( 'wp_enqueue_scripts', array( $this, 'frontend_assets' ) ); 123 | 124 | $rest_request = isset( $_SERVER['REQUEST_URI'] ) && false !== strpos( wp_unslash( $_SERVER['REQUEST_URI'] ), 'wp-json/wc-bookings' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 125 | 126 | if ( is_admin() || $rest_request ) { 127 | add_action( 'init', array( $this, 'rest_admin_includes' ) ); 128 | } 129 | 130 | if ( is_admin() ) { 131 | add_action( 'init', array( $this, 'admin_includes' ), 10 ); 132 | add_action( 'woocommerce_product_duplicate', array( $this, 'woocommerce_duplicate_product' ), 10, 2 ); 133 | } 134 | 135 | add_action( 'shutdown', array( $this, 'install' ) ); 136 | } 137 | 138 | /** 139 | * Check dependencies. 140 | * 141 | * @return bool|WP_Error Returns true if dependencies are satisfied. Otherwise error. 142 | */ 143 | public function check_dependencies() { 144 | if ( $this->dependencies_check_result ) { 145 | return $this->dependencies_check_result; 146 | } 147 | 148 | require_once WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-dependencies.php'; 149 | try { 150 | WC_Accommodation_Dependencies::check_dependencies(); 151 | $this->dependencies_check_result = true; 152 | } catch ( Exception $e ) { 153 | if ( function_exists( 'deactivate_plugins' ) ) { 154 | deactivate_plugins( plugin_basename( $this->plugin_file ) ); 155 | } 156 | 157 | $this->dependencies_check_result = new WP_Error( 'unsatisfied_dependencies', $e->getMessage() ); 158 | add_action( 'admin_notices', array( $this, 'deactivate_notice' ) ); 159 | } 160 | 161 | return $this->dependencies_check_result; 162 | } 163 | 164 | 165 | /** 166 | * Admin notice when plugin is automatically deactivated. 167 | * 168 | * @return void 169 | */ 170 | public function deactivate_notice() { 171 | if ( is_wp_error( $this->dependencies_check_result ) ) { 172 | $error_message = esc_html( $this->dependencies_check_result->get_error_message() ); 173 | echo wp_kses_post( sprintf( '
%s %s
', wpautop( $error_message ), wpautop( 'Plugin deactivated.' ) ) ); 174 | } 175 | } 176 | 177 | /** 178 | * Localisation 179 | */ 180 | public function load_plugin_textdomain() { 181 | /** 182 | * Filter locale before loading the plugin's text domain. 183 | * 184 | * @since 1.0.2 185 | * 186 | * @param string $locale The plugin's current locale. 187 | * @param string $domain Text domain. Unique identifier for retrieving translated strings. 188 | */ 189 | $locale = apply_filters( 'plugin_locale', get_locale(), 'woocommerce-accommodation-bookings' ); 190 | $dir = trailingslashit( WP_LANG_DIR ); 191 | 192 | load_textdomain( 'woocommerce-accommodation-bookings', $dir . 'woocommerce-accommodation-bookings/woocommerce-accommodation-bookings-' . $locale . '.mo' ); 193 | load_plugin_textdomain( 'woocommerce-accommodation-bookings', false, dirname( plugin_basename( $this->plugin_file ) ) . '/languages/' ); 194 | } 195 | 196 | /** 197 | * WooCommerce fallback notice. 198 | * 199 | * @since 1.2.1 200 | */ 201 | public function missing_wc_notice() { 202 | /* translators: %s WC download URL link. */ 203 | echo '

' . sprintf( esc_html__( 'Accommodation Bookings requires WooCommerce to be installed and active. You can download %s here.', 'woocommerce-accommodation-bookings' ), 'WooCommerce' ) . '

'; 204 | } 205 | 206 | /** 207 | * Load Classes 208 | */ 209 | public function includes() { 210 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-product-accommodation-booking.php'; 211 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-product-accommodation-booking-resource.php'; 212 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-booking.php'; 213 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-booking-cart-manager.php'; 214 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-booking-date-picker.php'; 215 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-booking-product-tabs.php'; 216 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'class-wc-accommodation-booking-order-manager.php'; 217 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'integrations/class-wc-accommodation-booking-addons.php'; 218 | } 219 | 220 | /** 221 | * Include admin 222 | */ 223 | public function admin_includes() { 224 | // Return if WooCommerce class not found. 225 | if ( ! class_exists( 'WooCommerce' ) ) { 226 | add_action( 'admin_notices', array( $this, 'missing_wc_notice' ) ); 227 | return; 228 | } 229 | 230 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'admin/class-wc-accommodation-booking-admin-panels.php'; 231 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'admin/class-wc-accommodation-booking-admin-product-settings.php'; 232 | } 233 | 234 | /** 235 | * Include REST API and Admin functions' class. 236 | */ 237 | public function rest_admin_includes() { 238 | // Return if WooCommerce class not found. 239 | if ( ! class_exists( 'WooCommerce' ) ) { 240 | add_action( 'admin_notices', array( $this, 'missing_wc_notice' ) ); 241 | return; 242 | } 243 | 244 | include WC_ACCOMMODATION_BOOKINGS_INCLUDES_PATH . 'admin/class-wc-accommodation-booking-rest-and-admin.php'; 245 | } 246 | 247 | /** 248 | * Frontend booking form scripts 249 | */ 250 | public function frontend_assets() { 251 | $build_path = dirname( WC_ACCOMMODATION_BOOKINGS_MAIN_FILE ) . '/build'; 252 | $style_data = include $build_path . '/css/frontend.asset.php'; 253 | $script_data = include $build_path . '/js/frontend/booking-form.asset.php'; 254 | 255 | $script_dependencies = array_merge( 256 | $script_data['dependencies'], 257 | array( 'wc-bookings-booking-form' ) 258 | ); 259 | 260 | wp_enqueue_style( 261 | 'wc-accommodation-bookings-styles', 262 | WC_ACCOMMODATION_BOOKINGS_PLUGIN_URL . '/build/css/frontend.css', 263 | null, 264 | $style_data['version'] 265 | ); 266 | 267 | wp_enqueue_script( 268 | 'wc-accommodation-bookings-form', 269 | WC_ACCOMMODATION_BOOKINGS_PLUGIN_URL . '/build/js/frontend/booking-form.js', 270 | $script_dependencies, 271 | $script_data['version'], 272 | true 273 | ); 274 | 275 | wp_set_script_translations( 276 | 'wc-accommodation-bookings-form', 277 | 'woocommerce-accommodation-bookings', 278 | plugin_dir_path( WC_ACCOMMODATION_BOOKINGS_MAIN_FILE ) . '/languages' 279 | ); 280 | } 281 | 282 | /** 283 | * Show row meta on the plugin screen. 284 | * 285 | * @param array $links Plugin Row Meta. 286 | * @param string $file Plugin Base file. 287 | * 288 | * @return array 289 | */ 290 | public function plugin_row_meta( $links, $file ) { 291 | // phpcs:ignore 292 | if ( $file === plugin_basename( WC_ACCOMMODATION_BOOKINGS_MAIN_FILE ) ) { 293 | $row_meta = array( 294 | 'docs' => '' . __( 'Docs', 'woocommerce-accommodation-bookings' ) . '', //phpcs:ignore 295 | 'support' => '' . __( 'Premium Support', 'woocommerce-accommodation-bookings' ) . '', //phpcs:ignore 296 | ); 297 | 298 | return array_merge( $links, $row_meta ); 299 | } 300 | 301 | return (array) $links; 302 | } 303 | 304 | /** 305 | * Installer 306 | */ 307 | public function install() { 308 | global $wpdb; 309 | 310 | $force_update = false; 311 | $accommodation_bookings_version = get_option( 'wc_accommodation_bookings_version' ); 312 | 313 | if ( ! $accommodation_bookings_version ) { 314 | $force_update = true; 315 | $accommodation_bookings_version = $this->version; 316 | } 317 | 318 | // Data updates. 319 | if ( $force_update || version_compare( $accommodation_bookings_version, '1.1.3', '<' ) ) { 320 | $accommodation_bookings = $wpdb->get_results( "SELECT DISTINCT post_id FROM $wpdb->postmeta WHERE meta_key = '_wc_booking_pricing' AND meta_value LIKE '%override_block%';" ); 321 | foreach ( $accommodation_bookings as $accommodation_booking ) { 322 | $product = wc_get_product( $accommodation_booking->post_id ); 323 | 324 | if ( ! is_a( $product, 'WC_Product' ) ) { 325 | continue; 326 | } 327 | 328 | if ( 'accommodation-booking' !== $product->get_type() ) { 329 | continue; 330 | } 331 | 332 | $pricing = get_post_meta( $product->get_id(), '_wc_booking_pricing', true ); 333 | $original_base_cost = absint( get_post_meta( $product->get_id(), '_wc_booking_base_cost', true ) ); 334 | 335 | // Convert from the old to the new structure. 336 | foreach ( $pricing as &$pricing_row ) { 337 | $pricing_row['base_cost'] = $pricing_row['cost'] = 0; //phpcs:ignore 338 | $new_cost = $pricing_row['override_block']; 339 | unset( $pricing_row['override_block'] ); 340 | $pricing_row['base_modifier'] = $pricing_row['modifier'] = $new_cost > $original_base_cost ? 'plus' : 'minus'; //phpcs:ignore 341 | $pricing_row['cost'] = absint( $new_cost - $original_base_cost ); 342 | } 343 | 344 | update_post_meta( $product->get_id(), '_wc_booking_pricing', $pricing ); 345 | } 346 | } 347 | 348 | // Update version. 349 | update_option( 'wc_accommodation_bookings_version', $this->version ); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /includes/admin/class-wc-accommodation-booking-rest-and-admin.php: -------------------------------------------------------------------------------- 1 | 'issetyesno', 56 | '_wc_booking_person_qty_multiplier' => 'yesno', 57 | '_wc_booking_person_cost_multiplier' => 'yesno', 58 | '_wc_booking_min_persons_group' => 'int', 59 | '_wc_booking_max_persons_group' => 'int', 60 | '_wc_booking_has_person_types' => 'yesno', 61 | '_wc_booking_has_resources' => 'issetyesno', 62 | '_wc_booking_resources_assignment' => '', 63 | '_wc_booking_resouce_label' => '', 64 | '_wc_accommodation_booking_calendar_display_mode' => '', 65 | '_wc_accommodation_booking_requires_confirmation' => 'yesno', 66 | '_wc_accommodation_booking_user_can_cancel' => '', 67 | '_wc_accommodation_booking_cancel_limit' => 'int', 68 | '_wc_accommodation_booking_cancel_limit_unit' => '', 69 | '_wc_accommodation_booking_max_date' => 'max_date', 70 | '_wc_accommodation_booking_max_date_unit' => 'max_date_unit', 71 | '_wc_accommodation_booking_min_date' => 'int', 72 | '_wc_accommodation_booking_min_date_unit' => '', 73 | '_wc_accommodation_booking_qty' => 'int', 74 | '_wc_accommodation_booking_base_cost' => 'float', 75 | '_wc_accommodation_booking_display_cost' => '', 76 | '_wc_accommodation_booking_min_duration' => 'int', 77 | '_wc_accommodation_booking_max_duration' => 'int', 78 | ); 79 | 80 | foreach ( $meta_to_save as $meta_key => $sanitize ) { 81 | $value = sanitize_text_field( wp_unslash( $_POST[ $meta_key ] ?? '' ) ); 82 | switch ( $sanitize ) { 83 | case 'int': 84 | $value = $value ? absint( $value ) : ''; 85 | break; 86 | case 'float': 87 | $value = $value ? floatval( $value ) : ''; 88 | break; 89 | case 'yesno': 90 | $value = 'yes' === $value ? 'yes' : 'no'; 91 | break; 92 | case 'issetyesno': 93 | $value = $value ? 'yes' : 'no'; 94 | break; 95 | case 'max_date': 96 | $value = absint( $value ); 97 | if ( 0 === $value ) { 98 | $value = 1; 99 | } 100 | break; 101 | } 102 | 103 | $meta_key = str_replace( '_wc_accommodation_booking_', '_wc_booking_', $meta_key ); 104 | update_post_meta( $post_id, $meta_key, $value ); 105 | 106 | if ( '_wc_booking_display_cost' === $meta_key ) { 107 | update_post_meta( $post_id, '_wc_display_cost', $value ); 108 | } 109 | 110 | if ( '_wc_booking_base_cost' === $meta_key ) { 111 | update_post_meta( $post_id, '_wc_booking_block_cost', $value ); 112 | } 113 | } 114 | 115 | // Availability. 116 | $availability = array(); 117 | $row_size = isset( $_POST['wc_accommodation_booking_availability_type'] ) ? count( $_POST['wc_accommodation_booking_availability_type'] ) : 0; 118 | for ( $i = 0; $i < $row_size; $i++ ) { 119 | $availability[ $i ]['type'] = wc_clean( $_POST['wc_accommodation_booking_availability_type'][ $i ] ); 120 | $availability[ $i ]['bookable'] = wc_clean( $_POST['wc_accommodation_booking_availability_bookable'][ $i ] ); 121 | $availability[ $i ]['priority'] = intval( $_POST['wc_accommodation_booking_availability_priority'][ $i ] ); 122 | 123 | switch ( $availability[ $i ]['type'] ) { 124 | case 'custom': 125 | $availability[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_availability_from_date'][ $i ] ); 126 | $availability[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_availability_to_date'][ $i ] ); 127 | break; 128 | case 'months': 129 | $availability[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_availability_from_month'][ $i ] ); 130 | $availability[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_availability_to_month'][ $i ] ); 131 | break; 132 | case 'weeks': 133 | $availability[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_availability_from_week'][ $i ] ); 134 | $availability[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_availability_to_week'][ $i ] ); 135 | break; 136 | case 'days': 137 | $availability[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_availability_from_day_of_week'][ $i ] ); 138 | $availability[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_availability_to_day_of_week'][ $i ] ); 139 | break; 140 | } 141 | } 142 | update_post_meta( $post_id, '_wc_booking_availability', $availability ); 143 | 144 | // Restricted days. 145 | update_post_meta( $post_id, '_wc_booking_has_restricted_days', isset( $_POST['_wc_accommodation_booking_has_restricted_days'] ) ); 146 | $restricted_days = isset( $_POST['_wc_accommodation_booking_restricted_days'] ) ? wc_clean( $_POST['_wc_accommodation_booking_restricted_days'] ) : ''; 147 | update_post_meta( $post_id, '_wc_booking_restricted_days', $restricted_days ); 148 | 149 | // Resources. 150 | if ( isset( $_POST['resource_id'] ) && isset( $_POST['_wc_booking_has_resources'] ) ) { 151 | $resource_data = filter_input_array( 152 | INPUT_POST, 153 | array( 154 | 'resource_id' => array( 155 | 'filter' => FILTER_VALIDATE_INT, 156 | 'flags' => FILTER_REQUIRE_ARRAY, 157 | ), 158 | 'resource_menu_order' => array( 159 | 'filter' => FILTER_VALIDATE_INT, 160 | 'flags' => FILTER_REQUIRE_ARRAY, 161 | ), 162 | 'resource_cost' => array( 163 | 'filter' => FILTER_VALIDATE_FLOAT, 164 | 'flags' => FILTER_REQUIRE_ARRAY, 165 | ), 166 | 'resource_block_cost' => array( 167 | 'filter' => FILTER_VALIDATE_FLOAT, 168 | 'flags' => FILTER_REQUIRE_ARRAY, 169 | ), 170 | ) 171 | ); 172 | $resource_ids = $resource_data['resource_id']; 173 | $resource_menu_order = $resource_data['resource_menu_order']; 174 | $resource_base_cost = $resource_data['resource_cost']; 175 | $resource_block_cost = $resource_data['resource_block_cost']; 176 | 177 | $max_loop = max( array_keys( $resource_ids ) ); 178 | 179 | $resource_base_costs = array(); 180 | $resource_block_costs = array(); 181 | 182 | for ( $i = 0; $i <= $max_loop; $i++ ) { 183 | if ( ! isset( $resource_ids[ $i ] ) ) { 184 | continue; 185 | } 186 | 187 | $resource_id = absint( $resource_ids[ $i ] ); 188 | 189 | $wpdb->update( 190 | "{$wpdb->prefix}wc_booking_relationships", 191 | array( 192 | 'sort_order' => absint( $resource_menu_order[ $i ] ), 193 | ), 194 | array( 195 | 'product_id' => $post_id, 196 | 'resource_id' => $resource_id, 197 | ) 198 | ); 199 | 200 | $resource_base_costs[ $resource_id ] = wc_clean( $resource_base_cost[ $i ] ); 201 | $resource_block_costs[ $resource_id ] = wc_clean( $resource_block_cost[ $i ] ); 202 | } 203 | 204 | update_post_meta( $post_id, '_resource_base_costs', $resource_base_costs ); 205 | update_post_meta( $post_id, '_resource_block_costs', $resource_block_costs ); 206 | } 207 | 208 | // Rates. 209 | $pricing = array(); 210 | $original_base_cost = abs( (float) get_post_meta( $post_id, '_wc_booking_base_cost', true ) ); 211 | 212 | $row_size = isset( $_POST['wc_accommodation_booking_pricing_type'] ) ? count( $_POST['wc_accommodation_booking_pricing_type'] ) : 0; 213 | for ( $i = 0; $i < $row_size; $i++ ) { 214 | $pricing[ $i ]['base_cost'] = 0; 215 | $pricing[ $i ]['cost'] = 0; 216 | $pricing[ $i ]['type'] = wc_clean( $_POST['wc_accommodation_booking_pricing_type'][ $i ] ); 217 | $new_cost = abs( (float) wc_clean( $_POST['wc_accommodation_booking_pricing_block_cost'][ $i ] ) ); 218 | $pricing[ $i ]['base_modifier'] = $new_cost > $original_base_cost ? 'plus' : 'minus'; 219 | $pricing[ $i ]['modifier'] = $pricing[ $i ]['base_modifier']; 220 | $pricing[ $i ]['cost'] = abs( $new_cost - $original_base_cost ); 221 | 222 | switch ( $pricing[ $i ]['type'] ) { 223 | case 'custom': 224 | $pricing[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_pricing_from_date'][ $i ] ); 225 | $pricing[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_pricing_to_date'][ $i ] ); 226 | break; 227 | case 'months': 228 | $pricing[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_pricing_from_month'][ $i ] ); 229 | $pricing[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_pricing_to_month'][ $i ] ); 230 | break; 231 | case 'weeks': 232 | $pricing[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_pricing_from_week'][ $i ] ); 233 | $pricing[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_pricing_to_week'][ $i ] ); 234 | break; 235 | case 'days': 236 | $pricing[ $i ]['from'] = wc_clean( $_POST['wc_accommodation_booking_pricing_from_day_of_week'][ $i ] ); 237 | $pricing[ $i ]['to'] = wc_clean( $_POST['wc_accommodation_booking_pricing_to_day_of_week'][ $i ] ); 238 | break; 239 | } 240 | } 241 | 242 | // Person Types. 243 | if ( isset( $_POST['person_id'] ) && isset( $_POST['_wc_booking_has_persons'] ) ) { 244 | $person_data = filter_input_array( 245 | INPUT_POST, 246 | array( 247 | 'person_id' => array( 248 | 'filter' => FILTER_VALIDATE_INT, 249 | 'flags' => FILTER_REQUIRE_ARRAY, 250 | ), 251 | 'person_menu_order' => array( 252 | 'filter' => FILTER_VALIDATE_INT, 253 | 'flags' => FILTER_REQUIRE_ARRAY, 254 | ), 255 | 'person_name' => array( 256 | 'filter' => FILTER_DEFAULT, 257 | 'flags' => FILTER_REQUIRE_ARRAY, 258 | ), 259 | 'person_cost' => array( 260 | 'filter' => FILTER_VALIDATE_FLOAT, 261 | 'flags' => FILTER_REQUIRE_ARRAY, 262 | ), 263 | 'person_block_cost' => array( 264 | 'filter' => FILTER_VALIDATE_FLOAT, 265 | 'flags' => FILTER_REQUIRE_ARRAY, 266 | ), 267 | 'person_description' => array( 268 | 'filter' => FILTER_DEFAULT, 269 | 'flags' => FILTER_REQUIRE_ARRAY, 270 | ), 271 | 'person_min' => array( 272 | 'filter' => FILTER_VALIDATE_INT, 273 | 'flags' => FILTER_REQUIRE_ARRAY, 274 | ), 275 | 'person_max' => array( 276 | 'filter' => FILTER_VALIDATE_INT, 277 | 'flags' => FILTER_REQUIRE_ARRAY, 278 | ), 279 | ) 280 | ); 281 | $person_ids = $person_data['person_id']; 282 | $person_menu_order = $person_data['person_menu_order']; 283 | $person_name = $person_data['person_name']; 284 | $person_cost = $person_data['person_cost']; 285 | $person_block_cost = $person_data['person_block_cost']; 286 | $person_description = $person_data['person_description']; 287 | $person_min = $person_data['person_min']; 288 | $person_max = $person_data['person_max']; 289 | 290 | $max_loop = max( array_keys( $person_ids ) ); 291 | 292 | for ( $i = 0; $i <= $max_loop; $i++ ) { 293 | if ( ! isset( $person_ids[ $i ] ) ) { 294 | continue; 295 | } 296 | 297 | $person_id = absint( $person_ids[ $i ] ); 298 | 299 | if ( empty( $person_name[ $i ] ) ) { 300 | /* translators: %d: person type number */ 301 | $person_name[ $i ] = sprintf( __( 'Person Type #%d', 'woocommerce-accommodation-bookings' ), ( $i + 1 ) ); 302 | } 303 | 304 | wp_update_post( 305 | array( 306 | 'ID' => $person_id, 307 | 'post_title' => stripslashes( $person_name[ $i ] ), 308 | 'post_excerpt' => stripslashes( $person_description[ $i ] ), 309 | 'menu_order' => $person_menu_order[ $i ], 310 | ) 311 | ); 312 | 313 | update_post_meta( $person_id, 'cost', wc_clean( $person_cost[ $i ] ) ); 314 | update_post_meta( $person_id, 'block_cost', wc_clean( $person_block_cost[ $i ] ) ); 315 | update_post_meta( $person_id, 'min', wc_clean( $person_min[ $i ] ) ); 316 | update_post_meta( $person_id, 'max', wc_clean( $person_max[ $i ] ) ); 317 | } 318 | } 319 | 320 | update_post_meta( $post_id, '_wc_booking_pricing', $pricing ); 321 | update_post_meta( $post_id, '_wc_booking_cost', '' ); 322 | 323 | update_post_meta( $post_id, '_regular_price', '' ); 324 | update_post_meta( $post_id, '_sale_price', '' ); 325 | update_post_meta( $post_id, '_manage_stock', 'no' ); 326 | 327 | // Set price so filters work - using get_base_cost(). 328 | $product = wc_get_product( $post_id ); 329 | $base_cost = $product->get_base_cost(); 330 | update_post_meta( $post_id, '_price', $base_cost ); 331 | 332 | // Price has been set to cost * min_duration in meta_lookup table, needs to be updated to maintain consistency. 333 | $wpdb->update( 334 | $wpdb->prefix . 'wc_product_meta_lookup', 335 | array( 336 | 'min_price' => $base_cost, 337 | 'max_price' => $base_cost, 338 | ), 339 | array( 340 | 'product_id' => $post_id, 341 | ), 342 | '%d' 343 | ); 344 | } 345 | } 346 | 347 | new WC_Accommodation_Booking_REST_And_Admin(); 348 | -------------------------------------------------------------------------------- /includes/class-wc-product-accommodation-booking.php: -------------------------------------------------------------------------------- 1 | product_type = $this->get_type(); 51 | parent::__construct( $product ); 52 | 53 | $this->wc_booking_duration_type = 'customer'; 54 | $this->wc_booking_duration_unit = 'night'; 55 | $this->wc_booking_duration = 1; 56 | } 57 | 58 | 59 | /** 60 | * Get resource by ID. 61 | * Need to override this to return the proper resource class. 62 | * 63 | * @param int $id 64 | * 65 | * @return WC_Product_Booking_Resource object 66 | */ 67 | public function get_resource( $id ) { 68 | $resource = parent::get_resource( $id ); 69 | 70 | if ( $resource ) { 71 | $resource = new WC_Product_Accommodation_Booking_Resource( $id, $this->get_id() ); 72 | } 73 | 74 | return $resource; 75 | } 76 | 77 | /** 78 | * Override product type 79 | * 80 | * @return string 81 | */ 82 | public function get_type() { 83 | return 'accommodation-booking'; 84 | } 85 | 86 | /** 87 | * Get resources objects. 88 | * 89 | * @param WC_Product 90 | * 91 | * @return array( 92 | * type WC_Product_Accommodation_Booking_Resource 93 | * ) 94 | */ 95 | public function get_resources() { 96 | $product_resources = array(); 97 | 98 | foreach ( $this->get_resource_ids() as $resource_id ) { 99 | $product_resources[] = new WC_Product_Accommodation_Booking_Resource( $resource_id, $this->get_id() ); 100 | } 101 | 102 | return $product_resources; 103 | } 104 | 105 | /** 106 | * Tells Bookings that this product type is a bookings addon. 107 | * 108 | * @return boolean 109 | */ 110 | public function is_bookings_addon() { 111 | return true; 112 | } 113 | 114 | /** 115 | * Human readable version of the addon title 116 | * 117 | * @return string 118 | */ 119 | public function bookings_addon_title() { 120 | return __( 'Accommodation booking', 'woocommerce-accommodation-bookings' ); 121 | } 122 | 123 | /** 124 | * We want users to be able to select their range of dates 125 | * 126 | * @return boolean 127 | */ 128 | public function is_range_picker_enabled() { 129 | return apply_filters( 'woocommerce_accommodation_bookings_range_picker_enabled', true ); 130 | } 131 | 132 | /** 133 | * Customers define how many nights they want to stay. There is no concept 134 | * of "fixed" durations for accommodations. 135 | * 136 | * @param string $context 137 | * 138 | * @return string 139 | */ 140 | public function get_duration_type( $context = 'view' ) { 141 | return 'customer'; 142 | } 143 | 144 | /** 145 | * Our duration is nights instead of days 146 | * 147 | * @param string $context 148 | * 149 | * @return string 150 | */ 151 | public function get_duration_unit( $context = 'view' ) { 152 | return 'night'; 153 | } 154 | 155 | /** 156 | * Costs can vary depending on rates (weekend rates, etc) 157 | * In the future, addons like cots can also change cost. 158 | * 159 | * @return boolean 160 | */ 161 | public function has_additional_costs() { 162 | return true; 163 | } 164 | 165 | /** 166 | * By default, rooms will be available. 167 | * 168 | * @return boolean 169 | */ 170 | public function get_default_availability() { 171 | return true; 172 | } 173 | 174 | /** 175 | * Hotel rooms are a "virtual" product. No shipping is involved. 176 | * 177 | * @return boolean 178 | */ 179 | public function is_virtual() { 180 | return $this->get_prop( 'virtual' ); 181 | } 182 | 183 | /** 184 | * Get price HTML 185 | * 186 | * @param string $price 187 | * 188 | * @return string 189 | */ 190 | public function get_price_html( $price = '' ) { 191 | 192 | // If display cost is set - user wants that to be displayed 193 | $display_price = $this->get_display_cost(); 194 | if ( ! $display_price ) { 195 | $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); 196 | $display_price = $tax_display_mode == 'incl' ? wc_get_price_including_tax( 197 | $this, 198 | array( 199 | 'qty' => 1, 200 | 'price' => $this->get_price(), 201 | ) 202 | ) : wc_get_price_excluding_tax( 203 | $this, 204 | array( 205 | 'qty' => 1, 206 | 'price' => $this->get_price(), 207 | ) 208 | ); 209 | } 210 | 211 | if ( $display_price ) { 212 | if ( $this->has_additional_costs() || $this->get_display_cost() ) { 213 | /* translators: %s: price */ 214 | $price_html = sprintf( __( 'From %s per night', 'woocommerce-accommodation-bookings' ), wc_price( $display_price ) ) . $this->get_price_suffix(); 215 | } else { 216 | $price_html = wc_price( $display_price ) . $this->get_price_suffix(); 217 | } 218 | } elseif ( ! $this->has_additional_costs() ) { 219 | $price_html = __( 'Free', 'woocommerce-accommodation-bookings' ); 220 | } else { 221 | $price_html = ''; 222 | } 223 | return apply_filters( 'woocommerce_get_price_html', $price_html, $this ); 224 | } 225 | 226 | /** 227 | * Find available and booked blocks for specific resources (if any) and return them as array. 228 | * 229 | * @param array $blocks 230 | * @param array $intervals 231 | * @param integer $resource_id 232 | * @param integer $from The starting date for the set of blocks 233 | * @param integer $to 234 | * @return array 235 | */ 236 | public function get_time_slots( $blocks, $resource_id = 0, $from = 0, $to = 0, $include_sold_out = false ) { 237 | $bookable_product = $this; 238 | 239 | $product_id = $bookable_product->get_id(); 240 | $transient_name = 'book_ts_' . md5( http_build_query( array( $product_id, $resource_id, $from, $to ) ) ); 241 | $available_slots = get_transient( $transient_name ); 242 | $booking_slots_transient_keys = array_filter( (array) get_transient( 'booking_slots_transient_keys' ) ); 243 | 244 | if ( ! isset( $booking_slots_transient_keys[ $product_id ] ) ) { 245 | $booking_slots_transient_keys[ $product_id ] = array(); 246 | } 247 | 248 | $booking_slots_transient_keys[ $product_id ][] = $transient_name; 249 | 250 | // Give array of keys a long ttl because if it expires we won't be able to flush the keys when needed. 251 | // We can't use 0 to never expire because then WordPress will autoload the option on every page. 252 | set_transient( 'booking_slots_transient_keys', $booking_slots_transient_keys, YEAR_IN_SECONDS ); 253 | 254 | if ( false === $available_slots ) { 255 | if ( empty( $intervals ) ) { 256 | $interval = $bookable_product->get_min_duration(); 257 | $intervals = array( $interval, 1 ); 258 | } 259 | 260 | list( $interval, $base_interval ) = $intervals; 261 | 262 | if ( version_compare( WC_BOOKINGS_VERSION, '1.15.0', '<' ) ) { 263 | $existing_bookings = WC_Bookings_Controller::get_all_existing_bookings( $bookable_product, $from, $to ); 264 | } else { 265 | $existing_bookings = WC_Booking_Data_Store::get_all_existing_bookings( $bookable_product, $from, $to ); 266 | } 267 | 268 | $booking_resource = $resource_id ? $bookable_product->get_resource( $resource_id ) : null; 269 | $available_slots = array(); 270 | $has_qty = ! is_null( $booking_resource ) ? $booking_resource->has_qty() : false; 271 | $has_resources = $bookable_product->has_resources(); 272 | 273 | foreach ( $blocks as $block ) { 274 | $check_in = self::get_check_times( 'in', $product_id ); 275 | $check_out = self::get_check_times( 'out', $product_id ); 276 | // Blocks for accommodation products are initially calculated as days but the actuall time blocks are shifted by check in and checkout times. 277 | $block_start_time = strtotime( "{$check_in}", $block ); 278 | $block_end_time = strtotime( "{$check_out}", strtotime( '+1 days', $block ) ); 279 | $resources = array(); 280 | 281 | // Figure out how much qty have, either based on combined resource quantity, 282 | // single resource, or just product. 283 | if ( $has_resources && ( ! is_a( $booking_resource, 'WC_Product_Booking_Resource' ) || ! $has_qty ) ) { 284 | $available_qty = 0; 285 | 286 | foreach ( $bookable_product->get_resources() as $resource ) { 287 | 288 | if ( ! $bookable_product->check_availability_rules_against_time( $block_start_time, $block_end_time, $block, $resource->get_id() ) ) { 289 | continue; 290 | } 291 | 292 | $qty = $resource->get_qty(); 293 | $available_qty += $qty; 294 | $resources[ $resource->get_id() ] = $qty; 295 | } 296 | } elseif ( $has_resources && $has_qty ) { 297 | // Only include if it is available for this selection. We set this block to be bookable by default, unless some of the rules apply. 298 | if ( ! $bookable_product->check_availability_rules_against_time( $block_start_time, $block_end_time, $booking_resource->get_id() ) ) { 299 | continue; 300 | } 301 | 302 | $qty = $booking_resource->get_qty(); 303 | $available_qty = $qty; 304 | $resources[ $booking_resource->get_id() ] = $qty; 305 | } else { 306 | $available_qty = $bookable_product->get_qty(); 307 | $resources[0] = $bookable_product->get_qty(); 308 | } 309 | 310 | $qty_booked_in_block = 0; 311 | 312 | foreach ( $existing_bookings as $existing_booking ) { 313 | if ( $existing_booking->is_intersecting_block( $block_start_time, $block_end_time ) ) { 314 | $qty_to_add = $bookable_product->has_person_qty_multiplier() ? max( 1, array_sum( $existing_booking->get_persons() ) ) : 1; 315 | if ( $has_resources ) { 316 | if ( $existing_booking->get_resource_id() === absint( $resource_id ) ) { 317 | // Include the quantity to subtract if an existing booking matches the selected resource id 318 | $qty_booked_in_block += $qty_to_add; 319 | $resources[ $resource_id ] = ( isset( $resources[ $resource_id ] ) ? $resources[ $resource_id ] : 0 ) - $qty_to_add; 320 | } elseif ( ( is_null( $booking_resource ) || ! $has_qty ) && $existing_booking->get_resource() ) { 321 | // Include the quantity to subtract if the resource is auto selected (null/resource id empty) 322 | // but the existing booking includes a resource 323 | $qty_booked_in_block += $qty_to_add; 324 | $resources[ $existing_booking->get_resource_id() ] = ( isset( $resources[ $existing_booking->get_resource_id() ] ) ? $resources[ $existing_booking->get_resource_id() ] : 0 ) - $qty_to_add; 325 | } 326 | } else { 327 | $qty_booked_in_block += $qty_to_add; 328 | $resources[0] = ( isset( $resources[0] ) ? $resources[0] : 0 ) - $qty_to_add; 329 | } 330 | } 331 | } 332 | 333 | $available_slots[ $block ] = array( 334 | 'booked' => $qty_booked_in_block, 335 | 'available' => $available_qty - $qty_booked_in_block, 336 | 'resources' => $resources, 337 | ); 338 | } 339 | 340 | set_transient( $transient_name, $available_slots, YEAR_IN_SECONDS ); 341 | } 342 | 343 | return $available_slots; 344 | } 345 | 346 | 347 | /** 348 | * Get an array of blocks within in a specified date range 349 | * 350 | * The WC_Product_Booking class does not account for 'nights' as a valid duration unit so it retrieves every minute of each day as a block, 351 | * severly slowing down the load time of the page. 352 | * 353 | * @param $start_date 354 | * @param $end_date 355 | * @param array $intervals 356 | * @param int $resource_id 357 | * @param array $booked 358 | * @param bool $get_past_times 359 | * 360 | * @return array 361 | */ 362 | public function get_blocks_in_range( $start_date, $end_date, $intervals = array(), $resource_id = 0, $booked = array(), $get_past_times = false ) { 363 | 364 | $blocks_in_range = $this->get_blocks_in_range_for_day( $start_date, $end_date, $resource_id, $booked ); 365 | 366 | return array_unique( $blocks_in_range ); 367 | } 368 | 369 | /** 370 | * Get checkin and checkout times. 371 | * 372 | * @param string $type The type, check_in or check_out. 373 | * @param int $product_id The product ID. 374 | * 375 | * @return string The time, either from options or default or from the filtered value. 376 | */ 377 | public static function get_check_times( $type, $product_id = 0 ) { 378 | $option = get_option( 'woocommerce_accommodation_bookings_times_settings' ); 379 | $check_time = ''; 380 | 381 | switch ( $type ) { 382 | case 'in': 383 | $check_time = $option['check_in'] ?? '14:00'; 384 | break; 385 | case 'out': 386 | $check_time = $option['check_out'] ?? '14:00'; 387 | break; 388 | } 389 | 390 | /** 391 | * Filter the check-in/out times for a specific product. 392 | * 393 | * @param string $check_time The check-in/out time stored in the database. 394 | * @param string $type The type, check_in or check_out. 395 | * @param int $product_id The product ID. 396 | * 397 | * @return string The filtered/original time. 398 | */ 399 | return apply_filters( 'woocommerce_accommodation_booking_get_check_times', $check_time, $type, (int) $product_id ); 400 | } 401 | 402 | /** 403 | * Get duration. 404 | * 405 | * Duration unit is always one night. 406 | * 407 | * @param string $context 408 | * @return integer 409 | */ 410 | public function get_duration( $context = 'view' ) { 411 | return 1; 412 | } 413 | } 414 | 415 | endif; 416 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | *** Changelog *** 2 | 3 | = 1.3.6 - 2025-12-15 = 4 | * Fix - Ensure there is no fatal error on the bookings list page due to recent changes in Bookings V3. 5 | * Fix - JavasScript error reference to jQuery on New Product page. 6 | * Dev - Bump WooCommerce "tested up to" version 10.4. 7 | * Dev - Bump WooCommerce minimum supported version to 10.2. 8 | * Dev - Bump WordPress "tested up to" version 6.9. 9 | * Dev - Update PHP Compat sniffs to a development version covering most of the PHP 8.4 changes. 10 | * Dev - Run the PHP Compat sniffs against the entire production release of the plugin. 11 | * Dev - Updates to our QIT GitHub Action workflow. 12 | 13 | = 1.3.5 - 2025-10-06 = 14 | * Dev - Bump WooCommerce "tested up to" version 10.2. 15 | * Dev - Bump WooCommerce minimum supported version to 10.0. 16 | 17 | = 1.3.4 - 2025-08-11 = 18 | * Dev - Bump WooCommerce "tested up to" version 10.1. 19 | * Dev - Bump WooCommerce minimum supported version to 9.9. 20 | * Dev - Bump WordPress minimum supported version to 6.7. 21 | * Dev - Ensure JavaScript dependencies are set correctly. 22 | * Dev - Update NPM packages to fix security issues. 23 | 24 | = 1.3.3 - 2025-05-27 = 25 | * Dev - Bump WooCommerce "tested up to" version 9.9. 26 | * Dev - Bump WooCommerce minimum supported version to 9.7. 27 | * Dev - Bump WordPress "tested up to" version 6.8. 28 | * Dev - Update all third-party actions our workflows rely on to use versions based on specific commit hashes. 29 | 30 | = 1.3.2 - 2025-03-10 = 31 | * Update - Render uniform WP time picker over default time input element. 32 | * Dev - Bump WooCommerce "tested up to" version 9.7. 33 | * Dev - Bump WooCommerce minimum supported version to 9.5. 34 | * Dev - Bump WordPress minimum supported version to 6.6. 35 | * Dev - Disabled warning checks from WordPress Plugin Check Action. 36 | 37 | = 1.3.1 - 2025-01-20 = 38 | * Dev - Bump WooCommerce "tested up to" version 9.6. 39 | * Dev - Bump WooCommerce minimum supported version to 9.4. 40 | * Dev - Use the `@woocommerce/e2e-utils-playwright` NPM package for E2E tests. 41 | 42 | = 1.3.0 - 2024-12-02 = 43 | * Fix - Ensure sorting by price works as expected. 44 | * Dev - Bump WooCommerce "tested up to" version 9.5. 45 | * Dev - Bump WooCommerce minimum supported version to 9.3. 46 | 47 | = 1.2.10 - 2024-11-18 = 48 | * Dev - Bump WordPress "tested up to" version 6.7. 49 | 50 | = 1.2.9 - 2024-10-28 = 51 | * Dev - Bump WooCommerce "tested up to" version 9.4. 52 | * Dev - Bump WooCommerce minimum supported version to 9.2. 53 | * Dev - Bump WordPress minimum supported version to 6.5. 54 | 55 | = 1.2.8 - 2024-08-28 = 56 | * Fix - Ensure display of checkbox options shows correctly in an Accommodation product. 57 | * Fix - Allow Accommodation product tabs to be ordered properly. 58 | * Dev - Bump WooCommerce "tested up to" version 9.2. 59 | * Dev - Bump WooCommerce minimum supported version to 9.0. 60 | * Dev - Fix QIT E2E tests and add support for a few new test types. 61 | * Dev - Update E2E tests to accommodate the changes in WooCommerce 9.2. 62 | 63 | = 1.2.7 - 2024-07-15 = 64 | * Dev - Bump WooCommerce "tested up to" version 9.0. 65 | * Dev - Bump WooCommerce minimum supported version to 8.8. 66 | * Dev - Bump WordPress "tested up to" version 6.6. 67 | * Dev - Bump WordPress minimum supported version to 6.4. 68 | * Dev - Update NPM packages and node version to v20 to modernize developer experience. 69 | * Dev - Add proper plugin name to the `readme.txt`. Remove Bookings from our required plugins list as it's not hosted on WordPress.org. 70 | * Dev - Exclude the Woo Comment Hook `@since` sniff. 71 | * Fix - Unavailable accommodation days appear available even when booked. 72 | * Fix - Error in Updating Accommodation Product Data via REST API: `Unrecognized Product Type Response`. 73 | 74 | = 1.2.6 - 2024-05-20 = 75 | * Dev - Bump WooCommerce "tested up to" version 8.9. 76 | * Dev - Bump WooCommerce minimum supported version to 8.7. 77 | * Dev - Bump WordPress "tested up to" version 6.5. 78 | * Dev - Bump WordPress minimum supported version to 6.3. 79 | 80 | = 1.2.5 - 2024-02-26 = 81 | * Dev - Apply same code style (call static method) within file. 82 | * Dev - Bump WooCommerce "tested up to" version 8.6. 83 | * Dev - Bump WooCommerce minimum supported version to 8.4. 84 | * Fix - Booking Calendar displays incorrect availability for Accommodation Products. 85 | * Fix - Missing product ID when getting check-in time. 86 | 87 | = 1.2.4 - 2024-02-05 = 88 | * Dev - Bump PHP "tested up to" version 8.3. 89 | * Dev - Bump WooCommerce "tested up to" version 8.5. 90 | * Dev - Bump WooCommerce minimum supported version to 8.3. 91 | * Dev - Bump WordPress minimum supported version to 6.3. 92 | 93 | = 1.2.3 - 2024-01-08 = 94 | * Dev - Declare compatibility with WooCommerce Blocks. 95 | * Dev - Bump PHP minimum supported version to 7.4. 96 | * Dev - Bump WooCommerce "tested up to" version 8.4. 97 | * Dev - Bump WooCommerce minimum supported version to 8.2. 98 | 99 | = 1.2.2 - 2023-12-11 = 100 | * Dev - Add end-to-end tests using Playwright. 101 | * Dev - Update default behavior to use a block-based cart and checkout in E2E tests. 102 | * Dev - Bump WooCommerce "tested up to" version 8.3. 103 | * Dev - Bump WooCommerce minimum supported version to 8.1. 104 | * Dev - Bump WordPress "tested up to" version 6.4. 105 | * Dev - Bump WordPress minimum supported version to 6.2. 106 | 107 | = 1.2.1 - 2023-10-10 = 108 | * Dev - Hard code the paths to the asset data files. 109 | * Dev - Update PHPCS and PHPCompatibility GitHub Actions. 110 | * Fix - Fatal Error when WooCommerce is disabled. 111 | * Tweak - Indicate compatibility with WooPayments extension. 112 | 113 | = 1.2.0 - 2023-09-05 = 114 | * Dev - Bump PHP minimum supported version from 7.0 to 7.3. 115 | * Dev - Bump WooCommerce "tested up to" version from 7.8 to 8.0. 116 | * Dev - Bump WooCommerce minimum supported version from 7.2 to 7.8. 117 | * Dev - Bump WordPress "tested up to" version from 6.2 to 6.3. 118 | 119 | = 1.1.43 - 2023-07-17 = 120 | * Dev - Bump WooCommerce "tested up to" version 7.8. 121 | * Dev - Bump WooCommerce minimum supported version from 6.0 to 7.2. 122 | * Dev - Bump WordPress minimum supported version from 5.6 to 6.1. 123 | 124 | = 1.1.42 - 2023-06-14 = 125 | * Dev - Bump WooCommerce "tested up to" version to 7.6. 126 | * Dev - Bump WooCommerce minimum supported version from 6.0 to 6.8. 127 | * Dev - Bump WordPress "tested up to" version to 6.2. 128 | * Dev - Bump WordPress minimum supported version from 5.6 to 5.8. 129 | * Dev - Added new GitHub Workflow to run Quality Insights Toolkit tests. 130 | 131 | = 1.1.41 - 2023-05-26 = 132 | * Dev - Add product unit filter, `wc_bookings_product_duration_fallback`, to add night unit support. 133 | * Dev - Fix linting errors found by the Quality Insights Toolkit. 134 | 135 | = 1.1.40 - 2023-05-12 = 136 | * Dev - Added a new filter, `woocommerce_accommodation_booking_get_check_times`, to change the check-in/out timings per product. 137 | * Fix - Fully booked days show as partially booked - Day after booking shows partially booked. 138 | 139 | = 1.1.39 - 2023-03-14 = 140 | * Dev - Bump `http-cache-semantics` from 4.1.0 to 4.1.1. 141 | 142 | = 1.1.38 - 2023-03-13 = 143 | * Dev - Bump `http-cache-semantics` from 4.1.0 to 4.1.1. 144 | 145 | = 1.1.37 - 2023-01-27 = 146 | * Dev - Bump `scss-tokenizer` from 0.3.0 to 0.4.3 and `node-sass` from 7.0.1 to 7.0.3. 147 | * Fix - Fatal error when Rate is empty and Range Cost is added. 148 | * Tweak - Bump WooCommerce "tested up to" from 6.7 to 7.3. 149 | * Tweak - Bump WordPress tested up to version from 6.0 to 6.1. 150 | * Tweak - Bump minimum required WordPress version from 4.1 to 5.6. 151 | 152 | = 1.1.36 - 2022-11-24 = 153 | * Update - Bump NPM to v8. 154 | * Update - Bump composer to v2. 155 | * Update - Bump node to v16. 156 | 157 | = 1.1.35 - 2022-11-09 = 158 | * Add - Declare support for High-performance Order Storage ("HPOS"). 159 | * Bump minimist from 1.2.5 to 1.2.6. 160 | * Fix - Showing check-in date on the cart page instead of booking date. 161 | * Tweak - Minimum Version Bumps: WP 5.6, Woo 6.0, & PHP 7.0. 162 | 163 | = 1.1.34 - 2022-11-01 = 164 | * Fixed - PHP 8.0/8.1 Compatibility issue fixed: Critical error when cost in range is empty if Standard room rate is empty as well. 165 | 166 | = 1.1.33 - 2022-08-10 = 167 | * Fix - Build artifact includes node_modules. 168 | 169 | = 1.1.32 - 2022-08-01 = 170 | * Tweak - WC 6.7.0 compatibility. 171 | * Update all npm dependencies. 172 | 173 | = 1.1.31 - 2022-07-06 = 174 | * Fix - Fatal error with PHP 8.0 when Base Cost/Block Cost is empty. 175 | 176 | = 1.1.30 - 2022-06-02 = 177 | * Fix - PHP Warnings on the setting page. 178 | * Tweak - Bump tested up to WordPress version 6.0. 179 | 180 | = 1.1.29 - 2022-06-02 = 181 | * Fix - PHP Warnings on the setting page. 182 | * Tweak - Bump tested up to WordPress version 6.0. 183 | 184 | = 1.1.27 - 2022-06-02 = 185 | * Fix - PHP Warnings on the setting page. 186 | * Tweak - Bump tested up to WordPress version 6.0. 187 | 188 | = 1.1.26 - 2021-11-30 = 189 | * Fix - Bump y18n from 3.2.1 to 3.2.2. 190 | * Tweak - WC 5.9 compatibility. 191 | * Tweak - WP 5.8 compatibility. 192 | 193 | = 1.1.25 - 2021-10-28 = 194 | * Tweak - Change start days settings label to selectable days to be more accurate with functionality. 195 | * Tweak - WC 5.8 compatibility. 196 | * Tweak - WP 5.8 compatibility. 197 | 198 | = 1.1.24 - 2021-05-11 = 199 | * Fix - Added tested up to comment for WordPress compatibility to make it easier to use common tooling. 200 | * Fix - Replace deprecated jQuery 3 methods. 201 | 202 | = 1.1.23 - 2021-02-25 = 203 | * Fix - Dev - Fix: Add casts to float before applying the 'abs' function to potentially empty strings for compatibility with PHP8. 204 | * Tweak - WC 5.0 compatibility. 205 | 206 | = 1.1.22 - 2020-10-27 = 207 | * Tweak - WC 4.6 compatibility. 208 | 209 | = 1.1.21 - 2020-09-29 = 210 | * Fix - Skip formatting dates in ICS output for Accommodation Bookable products. 211 | * Fix - Allow products to be specified as virtual. 212 | * Fix - Use time for transient to skip autoload. 213 | * Tweak - Migrate Settings from Product to Bookings screen. 214 | 215 | = 1.1.20 - 2020-08-25 = 216 | * Fix - Do not round cost values in range types in Rates section. 217 | 218 | = 1.1.19 - 2020-08-19 = 219 | * Tweak - WordPress 5.5 compatibility. 220 | 221 | = 1.1.18 - 2020-07-08 = 222 | * Fix - Existing booking checkout date showed as fully booked and not selectable. 223 | 224 | = 1.1.17 - 2020-06-10 = 225 | * Tweak - WC 4.2 compatibility. 226 | 227 | = 1.1.16 - 2020-05-01 = 228 | * Tweak - WC 4.1 compatibility. 229 | 230 | = 1.1.15 - 2020-03-06 = 231 | * Add - Add basic unit tests suite. 232 | * Fix - Fix missing translation for resource duration display. 233 | * Tweak - WP tested up to 5.4. 234 | 235 | = 1.1.14 - 2020-02-26 = 236 | * Fix - Person types are not copied over when duplicating an existing accommodations product. 237 | * Tweak - WC tested up to 4.0. 238 | 239 | = 1.1.13 2020-02-04 = 240 | * Fix - Proper escaping of some attributes. 241 | * Fix - Ensure unavailable dates are shown to be unavailable in calendar. 242 | 243 | = 1.1.12 - 2020-01-15 = 244 | * Fix - Overlapping rate calculation is incorrect. 245 | 246 | = 1.1.11 - 2019-11-04 = 247 | * Tweak - WC tested up to 3.8. 248 | 249 | = 1.1.10 - 2019-10-24 = 250 | * Fix - Integration with WooCommerce Product Add-ons. 251 | 252 | = 1.1.9 - 2019-08-09 = 253 | * Tweak - Update deprecated calls to WC_Bookings_Controller::get_all_existing_bookings since Bookings 1.15.0. 254 | * Tweak - WC tested up to 3.7. 255 | 256 | = 1.1.8 - 2019-06-10 = 257 | * Fix - Adds accommodation bookings support for WooCommerce Bookings Availability extension 258 | 259 | = 1.1.7 - 2019-06-03 = 260 | * Fix - Mismatch function declaration causing PHP warnings. 261 | 262 | = 1.1.6 - 2019-04-17 = 263 | * Remove - partially booked days styling. 264 | * Tweak - WC tested up to 3.6 265 | 266 | = 1.1.5 - 2018-11-12 = 267 | * Fix - Overwrite get_duration function, to fix duration calculations. 268 | 269 | = 1.1.4 - 2018-10-23 = 270 | * Add - PAO 3.0 compatibility. 271 | * Fix - Check product object before calling method to prevent errors. 272 | 273 | = 1.1.3 = 274 | * Fix - Fatal error when disabling WooCommerce. 275 | * Fix - Undefined index notice. 276 | * Fix - Check for product before using it in order info. 277 | * Fix - Resource Costs not added for Custom Rate - Date Ranges. 278 | * Tweak - Add an option to autocomplete accommodation orders by allowing virtual accommodation products. 279 | * Fix - Remove duplicate person cost multiplier checkbox. 280 | 281 | = 1.1.2 = 282 | * Fix - Corrupted check-in/check-out dates in DB. 283 | * Update - WC 3.3 compatibility 284 | 285 | = 1.1.1 = 286 | * Fix - PHP Notice when fully booked array is empty. 287 | * Fix - Bookings outside of available range being checked for availability. 288 | * Fix - Maximum number of nights allowed option set to 0 breaks page. 289 | * Fix - Bookable product base cost not being cleared when changing to an Accommodation product. 290 | * Fix - Update _wc_booking_block_cost when base cost is updated. 291 | 292 | = 1.1.0 = 293 | * Fix - Display cost not used when presenting price to the client. 294 | * Add - Add woocommerce_accommodation_bookings_range_picker_enabled to disable range picker. 295 | * Fix - Fallback to default checkin/checkout time values if none are set. 296 | * Fix - Fully booked days not showing correctly when persons are used. 297 | * Fix - Resource availability calculated incorrectly. 298 | * Add - New functionality to restrict the day a booking can start on. 299 | * Fix - Tax fields missing. 300 | * Fix - Display price is showing incorrectly. 301 | * Fix - Availability rules being ignored. 302 | * Fix - Today should be shown as booked if it has check out available only 303 | 304 | = 1.0.9 = 305 | * Fix - Additional updates for WooCommerce 3.0 compatibility. 306 | * Fix - Min/max rules not being applied. 307 | * Fix - Prices not being added to cart. 308 | * Fix - Align panel icons. 309 | * Fix - Fatal error when using older version of Bookings. 310 | * Fix - Logic for availability of dates with checkin/checkout. 311 | * Fix - Resource costs not being calculated. 312 | 313 | = 1.0.8 = 314 | * Fix - WooCommerce 3.0 compatibility. 315 | 316 | = 1.0.7 = 317 | * Fix - Partially booked dates not indicated on the calendar. 318 | * Fix - Updated deprecated WooCommerce function calls. 319 | * Fix - Plugin locale/text domain loaded after output is generated. 320 | 321 | = 1.0.6 = 322 | * Fix - Check in/out time is not respected when booking is created 323 | * Tweak - Also check if booking class exists when plugin is loaded in case directory's name is not woocommerce-bookings 324 | 325 | = 1.0.5 = 326 | * Add - Display cost settings field. 327 | * Fix - Undefined variable on $post using tab manager. 328 | 329 | = 1.0.4 = 330 | * Feature - Add support for Persons 331 | * Feature - Add support for Resources 332 | * Fix - Full compatibility with Product Add-ons. 333 | 334 | = 1.0.3 = 335 | * Fix - Per Night Price Displaying Incorrect above bookings form. 336 | * Fix - Plugin on WordPress.org does not include assets folder. 337 | * Fix - Compatibility with Product Add-ons. 338 | 339 | = 1.0.2 = 340 | * Fix - Fatal Error on submit booking request in admin bookings 341 | * Fix - Incorrect end date time in order info and cart 342 | * Fix - No manual booking if product type is Accommodations 343 | * Fix - Better dependencies checker 344 | * Fix - Fatal error when plugin is deactivated 345 | * Fix - Fix display of checkout times when using different date formats. 346 | * Fix - Fix documentation link. 347 | 348 | = 1.0.1 = 349 | * Fix - Typo in month names. 350 | * Fix - Fix check-in/check-out times in the booking list table. 351 | 352 | = 1.0 = 353 | * Initial version. 354 | -------------------------------------------------------------------------------- /includes/class-wc-accommodation-booking-date-picker.php: -------------------------------------------------------------------------------- 1 | get_id() ); 42 | $check_out = WC_Product_Accommodation_Booking::get_check_times( 'out', $product->get_id() ); 43 | 44 | if ( 'night' === $product->get_duration_unit() ) { 45 | $data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} $check_in" ); 46 | $data['_end_date'] = strtotime( "+{$total_duration} day $check_out", $data['_start_date'] ); 47 | $data['_all_day'] = 0; 48 | } 49 | 50 | if ( $product->has_resources() && ! $product->is_resource_assignment_type( 'customer' ) ) { 51 | // Assign an available resource automatically. 52 | $available_bookings = wc_bookings_get_total_available_bookings_for_range( $product, $data['_start_date'], $data['_end_date'], 0, $data['_qty'] ); 53 | if ( is_array( $available_bookings ) ) { 54 | $data['_resource_id'] = current( array_keys( $available_bookings ) ); 55 | $data['type'] = get_the_title( current( array_keys( $available_bookings ) ) ); 56 | } 57 | } 58 | 59 | return $data; 60 | } 61 | 62 | /** 63 | * Changes the start label to "Check-in" 64 | * 65 | * @param string $label Label. 66 | * @return string 67 | */ 68 | public function start_label( $label ) { 69 | return __( 'Check-in', 'woocommerce-accommodation-bookings' ); 70 | } 71 | 72 | /** 73 | * Changes the end label to "Check-out" 74 | * 75 | * @param string $label Label. 76 | */ 77 | public function end_label( $label ) { 78 | return __( 'Check-out', 'woocommerce-accommodation-bookings' ); 79 | } 80 | 81 | /** 82 | * Add partially booked accomodation bookings 83 | * If a calendar date has check-in ( a booking starts on that date ) if it is feasible we want it marked as 84 | * partially booked because some other booking could end on that date. 85 | * If a calendar date has check-out ( a booking ends on that date ) if it is feasible we want it marked as 86 | * partially booked because some other booking could start on that date. 87 | * When it is feasible to mark a date partially booked: 88 | * - for a check-in date we check if a day before that date has any available resources. Only if a day before 89 | * check-in has any avaialble resources it is possible that some booking could end ( had its check-out ) on the 90 | * check-in date we are testing. 91 | * - for a check-out date we chack if that date has any available resources. If it does it means that some other 92 | * booking can start ( can have its check-in ) on the check-out date that we are testing. 93 | * Function works in followin steps: 94 | * 1. gather all check-in and checko-out dates for product 95 | * 2. loop over all dates from (1): 96 | * a. get all available resources for: day before for check-in and current for check-out 97 | * b. test if resources are available and if yes than move fully booked day to partially booked days 98 | * 99 | * @param array $booked_data_array Array of booked days. 100 | * @param WC_Product_Accommodation_Booking $product Product. 101 | */ 102 | public function add_partially_booked_dates( $booked_data_array, $product ) { 103 | // This function makes sense only for duration type: night. 104 | if ( 'night' !== $product->get_duration_unit() ) { 105 | return $booked_data_array; 106 | } 107 | 108 | // Start and the end dates of all bookings. 109 | $check_in_out_times = $this->get_check_in_and_out_times( $product ); 110 | 111 | // Go through each checkin and checkout days and mark them as partially booked. 112 | foreach ( array( 'in', 'out' ) as $which ) { 113 | foreach ( $check_in_out_times[ $which ] as $resource_id => $times ) { 114 | foreach ( $times as $time ) { 115 | $day = date( 'Y-n-j', $time ); 116 | if ( ! empty( $booked_data_array['partially_booked_days'][ $day ][ $resource_id ] ) ) { 117 | // The day is already partially booked so lets skip to the next day. 118 | continue; 119 | } 120 | 121 | $check_in_time = WC_Product_Accommodation_Booking::get_check_times( 'in', $product->get_id() ); 122 | if ( 'in' === $which ) { 123 | $check_time = strtotime( '-1 day ' . $check_in_time, $time ); 124 | } else { 125 | $check_time = strtotime( $check_in_time, $time ); 126 | } 127 | $check = date( 'F j, Y, g:i a', $check_time ); 128 | // Check available blocks for resource. If some are available that means that the day is not fully booked. 129 | $not_fully_booked = $this->get_product_resource_available_blocks_on_time( $product, $resource_id, $check_time ); 130 | if ( $not_fully_booked ) { 131 | $booked_data_array = $this->move_day_from_fully_to_partially_booked( $booked_data_array, $resource_id, $day ); 132 | } 133 | } 134 | } 135 | } 136 | 137 | return $booked_data_array; 138 | } 139 | 140 | /** 141 | * Update the fully booked, fully booked start day, and fully booked end day accomodation bookings, 142 | * 143 | * @param array $booked_data_array Array of booked days. 144 | * @param WC_Product_Accommodation_Booking $product Product. 145 | */ 146 | public function update_fully_booked_dates( $booked_data_array, $product ) { 147 | // This function makes sense only for duration type: night. 148 | if ( 'night' !== $product->get_duration_unit() ) { 149 | return $booked_data_array; 150 | } 151 | 152 | // Start and the end dates of all bookings. 153 | $check_in_out_times = $this->get_check_in_and_out_times( $product ); 154 | $res_auto_assign = $product->is_resource_assignment_type( 'automatic' ); 155 | 156 | // Go through each checkin and checkout days and mark them as fully booked. 157 | $made_partialy = array(); 158 | foreach ( array( 'in', 'out' ) as $which ) { 159 | foreach ( $check_in_out_times[ $which ] as $resource_id => $times ) { 160 | foreach ( $times as $time ) { 161 | $day = date( 'Y-n-j', $time ); 162 | 163 | $check_in_time = WC_Product_Accommodation_Booking::get_check_times( 'in', $product->get_id() ); 164 | if ( 'in' === $which ) { 165 | $check_time = strtotime( $check_in_time, $time ); 166 | } else { 167 | $check_time = strtotime( '-1 day' . $check_in_time, $time ); 168 | } 169 | $check = date( 'F j, Y, g:i a', $check_time ); 170 | // Check available blocks for resource. If some are available that means that the day is not fully booked. 171 | $resource_id_to_use = $res_auto_assign ? 0 : $resource_id; 172 | $available_on_time = $this->get_product_resource_available_blocks_on_time( $product, $resource_id_to_use, $check_time ); 173 | if ( 0 === $available_on_time ) { 174 | $booked_data_array = $this->prepare_fully_booked_start_and_end_days( $booked_data_array, $resource_id, $day, $which ); 175 | } else { 176 | $booked_data_array = $this->move_day_from_fully_to_partially_booked( $booked_data_array, $resource_id, $day ); 177 | $made_partialy[ $resource_id ] = $day; 178 | } 179 | } 180 | } 181 | } 182 | 183 | // Later removing the days from `fully_booked_days` array that were moved to partially booked days. 184 | // We are doing this out of the above foreach because we want a condition in 185 | // `prepare_fully_booked_start_and_end_days()` to be true. 186 | foreach ( $made_partialy as $resource => $partial_day ) { 187 | if ( ! isset( $booked_data_array['fully_booked_days'][ $partial_day ][ $resource ] ) ) { 188 | continue; 189 | } 190 | 191 | unset( $booked_data_array['fully_booked_days'][ $day ][ $resource ] ); 192 | 193 | if ( empty( $booked_data_array['fully_booked_days'][ $day ] ) ) { 194 | unset( $booked_data_array['fully_booked_days'][ $day ] ); 195 | } 196 | } 197 | 198 | return $booked_data_array; 199 | } 200 | 201 | /** 202 | * Should return find booked day blocks with additional data. 203 | * 204 | * @since 1.1.40 205 | * 206 | * @param array $result Array of booked days. 207 | * @param array $booked_blocks Array of booked blocks. 208 | */ 209 | public function find_booked_day_blocks( $result, $booked_blocks ): array { 210 | $result['fully_booked_start_days'] = $booked_blocks['fully_booked_start_days'] ?? array(); 211 | $result['fully_booked_end_days'] = $booked_blocks['fully_booked_end_days'] ?? array(); 212 | 213 | return $result; 214 | } 215 | 216 | /** 217 | * Calculates array that contains the start and the end time of all bookings for given product. 218 | * 219 | * @param \WC_Product_Booking $product Product. 220 | */ 221 | private function get_check_in_and_out_times( $product ) { 222 | 223 | $check_in_out_times = array( 224 | 'in' => array(), 225 | 'out' => array(), 226 | ); 227 | 228 | // Using all existing bookings we will calculate start and end time for each booking. 229 | // Those times will be considered for switching particular day from full to partially booked days. 230 | if ( version_compare( WC_BOOKINGS_VERSION, '1.15.0', '<' ) ) { 231 | $existing_bookings = WC_Bookings_Controller::get_all_existing_bookings( $product ); 232 | } else { 233 | $existing_bookings = WC_Booking_Data_Store::get_all_existing_bookings( $product ); 234 | } 235 | 236 | foreach ( $existing_bookings as $booking ) { 237 | 238 | $resource = $booking->get_resource_id(); 239 | if ( ! array_key_exists( $resource, $check_in_out_times['in'] ) ) { 240 | $check_in_out_times['in'][ $resource ] = array(); 241 | $check_in_out_times['out'][ $resource ] = array(); 242 | } 243 | 244 | if ( ! in_array( $booking->start, $check_in_out_times['in'][ $resource ] ) ) { 245 | $check_in_out_times['in'][ $resource ][] = $booking->start; 246 | } 247 | 248 | if ( ! in_array( $booking->end, $check_in_out_times['out'][ $resource ] ) ) { 249 | $check_in_out_times['out'][ $resource ][] = $booking->end; 250 | } 251 | } 252 | 253 | return $check_in_out_times; 254 | } 255 | 256 | /** 257 | * Get amount of available product resoureces on a specific timestamp 258 | * 259 | * @param \WC_Product_Booking $product Product. 260 | * @param int $resource Resource ID. 261 | * @param string $time Timestamp. 262 | * 263 | * @return int|mixed 264 | */ 265 | private function get_product_resource_available_blocks_on_time( $product, $resource, $time ) { 266 | $blocks = $product->get_blocks_in_range_for_day( $time, $time, $resource, array() ); 267 | $available_blocks = wc_bookings_get_time_slots( $product, $blocks, array(), $resource, $time, $time ); 268 | return ! empty( $available_blocks[ $time ] ) ? $available_blocks[ $time ]['available'] : 0; 269 | } 270 | 271 | /** 272 | * Moves day from fully booked days array to partially booked days array and if the fully booked days is 273 | * array for that day is empty ( no assigned resources ) removes that empty day entry 274 | * 275 | * @param array $booked_data_array Array of booked days. 276 | * @param int $resource Resource ID. 277 | * @param string $day Day. 278 | */ 279 | private function move_day_from_fully_to_partially_booked( $booked_data_array, $resource, $day ) { 280 | if ( ! isset( $booked_data_array['fully_booked_days'][ $day ][ $resource ] ) ) { 281 | return $booked_data_array; 282 | } 283 | 284 | $booked_data_array['partially_booked_days'][ $day ][ $resource ] = $booked_data_array['fully_booked_days'][ $day ][ $resource ]; 285 | 286 | return $booked_data_array; 287 | } 288 | 289 | /** 290 | * Moves day from fully_booked_days array to the fully_booked_start_days or fully_booked_end_days 291 | * This is required because we want to showcase days availability as per the selection. 292 | * So by default, fully_booked_start_days will be colored red; not available for check-ins. 293 | * When start date is selected, fully_booked_end_days will be colored red; not available for check-outs. 294 | * 295 | * @param array $booked_data_array Array of booked days. 296 | * @param int $resource Resource ID. 297 | * @param string $day A Day. 298 | * @param string $which In or Out. 299 | */ 300 | private function prepare_fully_booked_start_and_end_days( $booked_data_array, $resource, $day, $which = 'in' ) { 301 | if ( isset( $booked_data_array['fully_booked_days'][ $day ][ $resource ] ) ) { 302 | if ( 'in' === $which ) { 303 | $booked_data_array['fully_booked_start_days'][ $day ][ $resource ] = $booked_data_array['fully_booked_days'][ $day ][ $resource ]; 304 | } else { 305 | $booked_data_array['fully_booked_end_days'][ $day ][ $resource ] = $booked_data_array['fully_booked_days'][ $day ][ $resource ]; 306 | } 307 | unset( $booked_data_array['fully_booked_days'][ $day ][ $resource ] ); 308 | 309 | if ( empty( $booked_data_array['fully_booked_days'][ $day ] ) ) { 310 | unset( $booked_data_array['fully_booked_days'][ $day ] ); 311 | } 312 | } else { 313 | // If the day already exists fully_booked_start_days at this point, 314 | // it means it is also a fully booked end day, so reverting it 315 | // back to be in fully_booked_days. 316 | if ( isset( $booked_data_array['fully_booked_start_days'][ $day ][ $resource ] ) ) { 317 | $booked_data_array['fully_booked_days'][ $day ][ $resource ] = $booked_data_array['fully_booked_start_days'][ $day ][ $resource ]; 318 | unset( $booked_data_array['fully_booked_start_days'][ $day ][ $resource ] ); 319 | 320 | if ( empty( $booked_data_array['fully_booked_start_days'][ $day ] ) ) { 321 | unset( $booked_data_array['fully_booked_start_days'][ $day ] ); 322 | } 323 | } 324 | } 325 | 326 | // Also removing from partially booked array in case if it 327 | // was added there in `move_day_from_fully_to_partially_booked()`. 328 | if ( isset( $booked_data_array['partially_booked_days'][ $day ][ $resource ] ) ) { 329 | unset( $booked_data_array['partially_booked_days'][ $day ][ $resource ] ); 330 | } 331 | if ( empty( $booked_data_array['partially_booked_days'][ $day ] ) ) { 332 | unset( $booked_data_array['partially_booked_days'][ $day ] ); 333 | } 334 | 335 | return $booked_data_array; 336 | } 337 | } 338 | 339 | new WC_Accommodation_Booking_Date_Picker(); 340 | --------------------------------------------------------------------------------