' . 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 |
--------------------------------------------------------------------------------