Local Pickup: add support for addresses and base taxes on them

Break address into multiple pieces for pickup Break address into multiple pieces for pickup Break address into multiple pieces for pickup locations

Apply taxes based on location

Require locations

Customer location tax logic

Improved editing
This commit is contained in:
Mike Jolley 2022-10-19 21:40:57 +01:00 committed by Nadir Seghir
parent 586bc90f1b
commit 87c51d822f
3 changed files with 218 additions and 30 deletions

View File

@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\Blocks\Shipping;
use WC_Shipping_Method;
use WC_Order;
use Automattic\WooCommerce\Utilities\ArrayUtil;
/**
* Local Pickup Shipping Method.
@ -42,13 +44,13 @@ class PickupLocation extends WC_Shipping_Method {
*/
public function calculate_shipping( $package = array() ) {
if ( $this->pickup_locations ) {
foreach ( $this->pickup_locations as $location ) {
foreach ( $this->pickup_locations as $index => $location ) {
if ( ! $location['enabled'] ) {
continue;
}
$this->add_rate(
array(
'id' => $this->id . ':' . sanitize_key( $location['name'] ),
'id' => $this->id . ':' . $index,
// This is the label shown in shipping rate/method context e.g. London (Local Pickup).
'label' => wp_kses_post( $location['name'] . ' (' . $this->title . ')' ),
'package' => $package,
@ -56,20 +58,11 @@ class PickupLocation extends WC_Shipping_Method {
'description' => $location['details'],
'meta_data' => array(
'pickup_location' => wp_kses_post( $location['name'] ),
'pickup_address' => $location['address'],
'pickup_address' => wc()->countries->get_formatted_address( $location['address'], ', ' ),
),
)
);
}
} else {
$this->add_rate(
array(
'id' => $this->id,
'label' => $this->title,
'package' => $package,
'cost' => $this->cost,
)
);
}
}
@ -134,7 +127,13 @@ class PickupLocation extends WC_Shipping_Method {
foreach ( $location_names as $index => $location_name ) {
$locations[] = [
'name' => $location_name,
'address' => wc_clean( wp_unslash( $_POST['locationAddress'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'address' => [
'address_1' => wc_clean( wp_unslash( $_POST['address_1'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'city' => wc_clean( wp_unslash( $_POST['city'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'state' => wc_clean( wp_unslash( $_POST['state'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'postcode' => wc_clean( wp_unslash( $_POST['postcode'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'country' => wc_clean( wp_unslash( $_POST['country'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
],
'details' => wc_clean( wp_unslash( $_POST['details'][ $index ] ?? '' ) ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'enabled' => wc_string_to_bool( wc_clean( wp_unslash( $_POST['locationEnabled'][ $index ] ?? 1 ) ) ) ? 1 : 0, // phpcs:ignore WordPress.Security.NonceVerification.Missing
];
@ -250,22 +249,28 @@ class PickupLocation extends WC_Shipping_Method {
.wc-local-pickup-locations .wc-local-pickup-location-address,
.wc-local-pickup-locations .wc-local-pickup-location-details {
width: 25%;
padding-top: 10px !important;
padding-bottom: 10px !important;
}
#pickup_locations .wc-local-pickup-locations .editable input {
border-color: transparent;
background: transparent;
width: 100%;
padding: 0;
margin: 0;
height: auto;
min-height: auto;
line-height: 24px;
#pickup_locations .wc-local-pickup-locations .editing .view {
display: none;
}
#pickup_locations .wc-local-pickup-locations .editing .edit {
display: block;
}
#pickup_locations .wc-local-pickup-locations .editable input,
#pickup_locations .wc-local-pickup-locations .editable textarea,
#pickup_locations .wc-local-pickup-locations .editable select {
vertical-align: middle;
text-overflow: ellipsis;
width: 100%;
margin: 2px 0;
}
#pickup_locations .wc-local-pickup-locations .editable input:focus {
background: transparent;
padding: 0 8px;
#pickup_locations .wc-local-pickup-locations .editable textarea {
padding: 5px;
}
#pickup_locations .wc-local-pickup-locations tr .row-actions {
position: relative;
}
@ -292,9 +297,34 @@ class PickupLocation extends WC_Shipping_Method {
}, false );
locationsTable.addEventListener( "click", function(event) {
const deleteButton = event.target.closest('button.delete');
const editButton = event.target.closest('button.button-link-edit');
const deleteButton = event.target.closest('button.button-link-delete');
const enabledToggleButton = event.target.closest('button.enabled-toggle-button');
if (editButton !== null) {
const toggleText = editButton.dataset.toggle;
const innerText = editButton.innerText;
const tr = editButton.parentElement.parentElement.parentElement;
if ( tr.classList.contains('editing') ) {
tr.querySelectorAll('.edit').forEach( function( element ) {
const newValues = [];
element.querySelectorAll('.edit input[name], .edit textarea[name], .edit select[name]').forEach( function( input ) {
const newValue = input.value;
if ( newValue ) {
newValues.push( input.value );
}
});
element.parentElement.querySelector('.view').innerText = newValues.join(', ') || '—';
});
}
editButton.innerText = toggleText;
editButton.dataset.toggle = innerText;
editButton.parentElement.parentElement.parentElement.classList.toggle('editing');
return false;
}
if (deleteButton !== null) {
deleteButton.parentElement.parentElement.parentElement.remove();
return false;
@ -320,6 +350,77 @@ class PickupLocation extends WC_Shipping_Method {
return true;
}, false );
locationsTable.addEventListener( "focus", function(event) {
const input = event.target.closest('input, select');
if (input !== null) {
input.parentElement.classList.add("is-active");
}
}, true );
locationsTable.addEventListener( "blur", function(event) {
const input = event.target.closest('input, select');
if (input !== null) {
const nextInput = event.relatedTarget ? event.relatedTarget.closest('input, select') : null;
if(nextInput === null || nextInput.parentElement !== input.parentElement) {
input.parentElement.classList.remove("is-active");
}
}
}, true );
var states = JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( WC()->countries->get_states() ) ); ?>' ) );
locationsTable.addEventListener( "change", function(event) {
const countrySelect = event.target.closest('select.country-select');
if (countrySelect === null) {
return;
}
const stateInput = countrySelect.parentElement.querySelectorAll('input.state-input')[0];
const stateSelect = countrySelect.parentElement.querySelectorAll('select.state-select')[0];
const selectedCountry = countrySelect.value;
const selectedState = stateInput.value;
if ( selectedCountry === "" ) {
countrySelect.classList.add("placeholder");
} else {
countrySelect.classList.remove("placeholder");
}
if (states[selectedCountry] === undefined || states[selectedCountry].length === 0) {
stateSelect.hidden = true;
stateInput.type = 'text';
return;
}
stateSelect.innerHTML = '';
for (const [key, value] of Object.entries(states[selectedCountry])) {
const option = document.createElement("option");
option.value = key;
option.text = value;
option.selected = selectedState === key;
stateSelect.add( option );
};
stateSelect.hidden = false;
stateInput.type = 'hidden';
}, true );
locationsTable.addEventListener( "change", function(event) {
const stateSelect = event.target.closest('select.state-select');
if (stateSelect === null) {
return;
}
const stateInput = stateSelect.parentElement.querySelectorAll('input.state-input')[0];
stateInput.value = stateSelect.value;
}, true );
var event = new Event('change');
locationsTable.querySelectorAll('select.country-select').forEach(function(countrySelect) {
countrySelect.dispatchEvent(event);
});
</script>
<?php
}
@ -332,12 +433,31 @@ class PickupLocation extends WC_Shipping_Method {
*/
protected function pickup_location_row( $location = [] ) {
ob_start();
$location = wp_parse_args(
$location,
[
'name' => '',
'enabled' => false,
'details' => '',
]
);
$location['address'] = wp_parse_args(
$location['address'] ?? [],
[
'address_1' => '',
'city' => '',
'state' => '',
'postcode' => '',
'country' => '',
]
);
?>
<td width="1%" class="wc-local-pickup-location-sort sort"></td>
<td class="wc-local-pickup-location-name editable">
<input type="text" name="locationName[]" value="<?php echo esc_attr( $location['name'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'New location', 'woo-gutenberg-products-block' ); ?>" />
<div class="view"><?php echo esc_html( $location['name'] ?? '' ); ?></div>
<div class="edit" hidden><input type="text" name="locationName[]" value="<?php echo esc_attr( $location['name'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'New location', 'woo-gutenberg-products-block' ); ?>" /></div>
<div class="row-actions">
<button type="button" class="delete button-link button-link-delete"><?php esc_html_e( 'Delete', 'woo-gutenberg-products-block' ); ?></button>
<button type="button" class="button-link-edit button-link" data-toggle="<?php esc_attr_e( 'Done', 'woo-gutenberg-products-block' ); ?>"><?php esc_html_e( 'Edit', 'woo-gutenberg-products-block' ); ?></button> | <button type="button" class="button-link button-link-delete"><?php esc_html_e( 'Delete', 'woo-gutenberg-products-block' ); ?></button>
</div>
</td>
<td width="1%" class="wc-local-pickup-location-enabled">
@ -350,10 +470,24 @@ class PickupLocation extends WC_Shipping_Method {
</button>
</td>
<td class="wc-local-pickup-location-address editable">
<input type="text" name="locationAddress[]" value="<?php echo esc_attr( $location['address'] ?? '' ); ?>" placeholder="&mdash;" />
<div class="view"><?php echo esc_html( implode( ', ', array_filter( $location['address'] ) ) ); ?></div>
<div class="edit" hidden>
<input type="text" name="address_1[]" value="<?php echo esc_attr( $location['address']['address_1'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'Address', 'woo-gutenberg-products-block' ); ?>" />
<input type="text" name="city[]" value="<?php echo esc_attr( $location['address']['city'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'City', 'woo-gutenberg-products-block' ); ?>" />
<select class="state-select" hidden></select>
<input type="text" class="state-input" name="state[]" value="<?php echo esc_attr( $location['address']['state'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'State', 'woo-gutenberg-products-block' ); ?>" />
<input type="text" name="postcode[]" value="<?php echo esc_attr( $location['address']['postcode'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'Postcode / ZIP', 'woo-gutenberg-products-block' ); ?>" />
<select class="country-select" name="country[]">
<option value="" disabled selected><?php esc_html_e( 'Country', 'woo-gutenberg-products-block' ); ?></option>
<?php foreach ( WC()->countries->get_countries() as $code => $label ) : ?>
<option <?php selected( $code, $location['address']['country'] ); ?> value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
</div>
</td>
<td class="wc-local-pickup-location-details editable">
<input type="text" name="details[]" value="<?php echo esc_attr( $location['details'] ?? '' ); ?>" placeholder="&mdash;" />
<div class="view"><?php echo wp_kses_post( wpautop( $location['details'] ?: '&mdash;' ) ); ?></div>
<div class="edit" hidden><textarea name="details[]" rows="3"><?php echo esc_attr( $location['details'] ?? '' ); ?></textarea></div>
</td>
<?php
return ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

View File

@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks\Shipping;
use Automattic\WooCommerce\StoreApi\Utilities\CartController;
/**
* ShippingController class.
*
@ -12,6 +14,7 @@ class ShippingController {
*/
public function init() {
add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_shipping_methods' ) );
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'handle_customer_taxable_address' ) );
}
/**
@ -21,4 +24,54 @@ class ShippingController {
$pickup = new PickupLocation();
wc()->shipping->register_shipping_method( $pickup );
}
/**
* Filter the location used for taxes based on the chosen pickup location.
*
* @param array $address Location args.
* @return array
*/
public function handle_customer_taxable_address( $address ) {
$controller = new CartController();
$packages = $controller->get_shipping_packages( false );
$selected_rates = wc()->session->get( 'chosen_shipping_methods', array() );
$pickup_locations = get_option( 'pickup_location_pickup_locations', [] );
if ( empty( $packages ) || empty( $selected_rates ) || empty( $pickup_locations ) ) {
return $address;
}
$location_ids = [];
// Require pickup for all packages.
foreach ( $packages as $package_id => $package ) {
$selected_rate = $selected_rates[ $package_id ] ?? '';
$method_id = explode( ':', $selected_rate )[0];
if ( 'pickup_location' !== $method_id ) {
return $address;
}
$location_ids[] = explode( ':', $selected_rate )[1] ?? null;
}
$location_ids = array_unique( $location_ids );
if ( count( $location_ids ) > 1 ) {
return $address;
}
$location_id = $location_ids[0];
if ( ! empty( $pickup_locations[ $location_id ] ) && ! empty( $pickup_locations[ $location_id ]['address']['country'] ) ) {
return array(
$pickup_locations[ $location_id ]['address']['country'],
$pickup_locations[ $location_id ]['address']['state'],
$pickup_locations[ $location_id ]['address']['postcode'],
$pickup_locations[ $location_id ]['address']['city'],
);
}
return $address;
}
}

View File

@ -454,6 +454,7 @@ class OrderController {
return $location;
}
/**
* Create order line items.
*