diff --git a/modules/tax/commerce_tax.module b/modules/tax/commerce_tax.module index 23d00d261c..cf6fcc31ef 100644 --- a/modules/tax/commerce_tax.module +++ b/modules/tax/commerce_tax.module @@ -40,6 +40,11 @@ function commerce_tax_entity_base_field_info(EntityTypeInterface $entity_type) { ->setDisplayConfigurable('view', TRUE) ->setDisplayConfigurable('form', TRUE); + $tax_plugin_manager = \Drupal::service('plugin.manager.commerce_tax_type'); + foreach ($tax_plugin_manager->getDefinitions() as $definition) { + $plugin = $tax_plugin_manager->createInstance($definition['id']); + $fields += $plugin->storeFields(); + } return $fields; } } @@ -58,6 +63,23 @@ function commerce_tax_form_commerce_store_form_alter(&$form, FormStateInterface ]; $form['prices_include_tax']['#group'] = 'tax_settings'; $form['tax_registrations']['#group'] = 'tax_settings'; + $settings = []; + $settings['#group'] = 'tax_settings'; + + // @todo Fix when https://www.drupal.org/project/drupal/issues/1149078 has + // landed. Ideally these should only show up for stores that have selected + // CA in tax registrations. However, the States API is broken for select + // elements with #multiple => TRUE. + // $settings['#states'] = [ + // 'visible' => [ + // ':input[name^="tax_registrations"]' => ['value' => 'CA'], + // ] + // ]; + + $form['tax_ca_gst_number'] += $settings; + $form['tax_ca_pst_bc_number'] += $settings; + $form['tax_ca_pst_mb_number'] += $settings; + $form['tax_ca_qst_number'] += $settings; } } diff --git a/modules/tax/src/Plugin/Commerce/TaxType/CanadianSalesTax.php b/modules/tax/src/Plugin/Commerce/TaxType/CanadianSalesTax.php index 1c23242739..d5b9238591 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/CanadianSalesTax.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/CanadianSalesTax.php @@ -5,6 +5,7 @@ use Drupal\commerce_order\Entity\OrderItemInterface; use Drupal\commerce_store\Entity\StoreInterface; use Drupal\commerce_tax\TaxZone; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; use Drupal\profile\Entity\ProfileInterface; @@ -41,18 +42,24 @@ protected function matchesAddress(StoreInterface $store) { protected function matchesRegistrations(StoreInterface $store) { $store_registrations = $store->get('tax_registrations')->getValue(); $store_registrations = array_column($store_registrations, 'value'); - return in_array('CA', $store_registrations); + if (in_array('CA', $store_registrations)) { + foreach ($this->getZones() as $zone) { + if ($zone->isRegistered($store)) { + return TRUE; + } + } + } } /** * {@inheritdoc} */ - protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile) { + protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile, StoreInterface $store) { $customer_address = $customer_profile->get('address')->first(); if ($customer_address->getCountryCode() != 'CA') { return []; } - return parent::resolveZones($order_item, $customer_profile); + return parent::resolveZones($order_item, $customer_profile, $store); } /** @@ -85,6 +92,7 @@ public function buildZones() { 'default' => TRUE, ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['bc'] = new TaxZone([ 'id' => 'bc', @@ -102,6 +110,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_pst_bc_number', ]); $zones['mb'] = new TaxZone([ 'id' => 'mb', @@ -119,6 +128,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_pst_mb_number', ]); $zones['nb'] = new TaxZone([ 'id' => 'nb', @@ -136,6 +146,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['nl'] = new TaxZone([ 'id' => 'nl', @@ -153,6 +164,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['ns'] = new TaxZone([ 'id' => 'ns', @@ -170,6 +182,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['on'] = new TaxZone([ 'id' => 'on', @@ -187,6 +200,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['pe'] = new TaxZone([ 'id' => 'pe', @@ -204,6 +218,7 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_gst_number', ]); $zones['qc'] = new TaxZone([ 'id' => 'qc', @@ -221,9 +236,52 @@ public function buildZones() { ], ], ], + 'registration' => 'tax_ca_qst_number', ]); return $zones; } + /** + * {@inheritdoc} + */ + public function storeFields() { + $fields = []; + + $fields['tax_ca_gst_number'] = BaseFieldDefinition::create('string') + ->setLabel(t('Canada GST/HST #')) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 5, + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); + $fields['tax_ca_pst_bc_number'] = BaseFieldDefinition::create('string') + ->setLabel(t('British Columbia PST #')) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 5, + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); + $fields['tax_ca_pst_mb_number'] = BaseFieldDefinition::create('string') + ->setLabel(t('Manitoba PST #')) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 5, + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); + $fields['tax_ca_qst_number'] = BaseFieldDefinition::create('string') + ->setLabel(t('Quebec QST #')) + ->setDisplayOptions('form', [ + 'type' => 'textfield', + 'weight' => 5, + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); + + return $fields; + } + } diff --git a/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php b/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php index d3b2f9de68..ac30bda7bd 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php @@ -3,6 +3,7 @@ namespace Drupal\commerce_tax\Plugin\Commerce\TaxType; use Drupal\commerce_order\Entity\OrderItemInterface; +use Drupal\commerce_store\Entity\StoreInterface; use Drupal\commerce_tax\TaxableType; use Drupal\commerce_tax\TaxZone; use Drupal\Core\Form\FormStateInterface; @@ -33,7 +34,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta /** * {@inheritdoc} */ - protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile) { + protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile, StoreInterface $store) { $zones = $this->getZones(); $customer_address = $customer_profile->address->first(); $customer_country = $customer_address->getCountryCode(); diff --git a/modules/tax/src/Plugin/Commerce/TaxType/LocalTaxTypeBase.php b/modules/tax/src/Plugin/Commerce/TaxType/LocalTaxTypeBase.php index 58add7fa4d..843223f976 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/LocalTaxTypeBase.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/LocalTaxTypeBase.php @@ -93,7 +93,7 @@ public function shouldRound() { */ public function applies(OrderInterface $order) { $store = $order->getStore(); - return $this->matchesAddress($store) || $this->matchesRegistrations($store); + return $this->matchesAddress($store) || $this->matchesRegistrations($store, $order); } /** @@ -111,7 +111,8 @@ public function apply(OrderInterface $order) { } $adjustments = $order_item->getAdjustments(); - $rates = $this->resolveRates($order_item, $customer_profile); + $rates = $this->resolveRates($order_item, $customer_profile, $store); + // Don't overcharge a tax-exempt customer if the price is tax-inclusive. // A negative adjustment is added with the difference, and optionally // applied to the unit price in the TaxOrderProcessor. @@ -128,7 +129,7 @@ public function apply(OrderInterface $order) { }); if (empty($positive_tax_adjustments)) { $store_profile = $this->buildStoreProfile($store); - $rates = $this->resolveRates($order_item, $store_profile); + $rates = $this->resolveRates($order_item, $store_profile, $store); $negate = TRUE; } } @@ -210,7 +211,7 @@ protected function matchesAddress(StoreInterface $store) { */ protected function matchesRegistrations(StoreInterface $store) { foreach ($this->getZones() as $zone) { - if ($this->checkRegistrations($store, $zone)) { + if ($zone->isRegistered($store) && $this->checkRegistrations($store, $zone)) { return TRUE; } } @@ -244,13 +245,15 @@ protected function checkRegistrations(StoreInterface $store, TaxZone $zone) { * The order item. * @param \Drupal\profile\Entity\ProfileInterface $customer_profile * The customer profile. Contains the address and tax number. + * @param \Drupal\commerce_store\Entity\StoreInterface $store + * The store. * * @return \Drupal\commerce_tax\TaxRate[] * The tax rates, keyed by tax zone ID. */ - protected function resolveRates(OrderItemInterface $order_item, ProfileInterface $customer_profile) { + protected function resolveRates(OrderItemInterface $order_item, ProfileInterface $customer_profile, StoreInterface $store) { $rates = []; - $zones = $this->resolveZones($order_item, $customer_profile); + $zones = $this->resolveZones($order_item, $customer_profile, $store); foreach ($zones as $zone) { $rate = $this->chainRateResolver->resolve($zone, $order_item, $customer_profile); if (is_object($rate)) { @@ -267,15 +270,17 @@ protected function resolveRates(OrderItemInterface $order_item, ProfileInterface * The order item. * @param \Drupal\profile\Entity\ProfileInterface $customer_profile * The customer profile. Contains the address and tax number. + * @param \Drupal\commerce_store\Entity\StoreInterface $store + * The store. * * @return \Drupal\commerce_tax\TaxZone[] * The tax zones. */ - protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile) { + protected function resolveZones(OrderItemInterface $order_item, ProfileInterface $customer_profile, StoreInterface $store) { $customer_address = $customer_profile->get('address')->first(); $resolved_zones = []; foreach ($this->getZones() as $zone) { - if ($zone->match($customer_address)) { + if ($zone->match($customer_address) && $zone->isRegistered($store)) { $resolved_zones[] = $zone; } } diff --git a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php index 6fed8212c1..492f0bb74c 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php @@ -261,4 +261,11 @@ protected function buildStoreProfile(StoreInterface $store) { return $this->storeProfiles[$store_id]; } + /** + * {@inheritdoc} + */ + public function storeFields() { + return []; + } + } diff --git a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeInterface.php b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeInterface.php index 3f64ea4f8f..6f381d8207 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeInterface.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeInterface.php @@ -62,4 +62,12 @@ public function applies(OrderInterface $order); */ public function apply(OrderInterface $order); + /** + * Generate store fields specific to this tax type. + * + * @return array + * An array of fields to add to the store entity type. + */ + public function storeFields(); + } diff --git a/modules/tax/src/TaxZone.php b/modules/tax/src/TaxZone.php index 2037b33621..75313cd0e2 100644 --- a/modules/tax/src/TaxZone.php +++ b/modules/tax/src/TaxZone.php @@ -4,6 +4,7 @@ use CommerceGuys\Addressing\AddressInterface; use CommerceGuys\Addressing\Zone\ZoneTerritory; +use Drupal\commerce_store\Entity\StoreInterface; /** * Represents a tax zone. @@ -45,6 +46,13 @@ class TaxZone { */ protected $rates; + /** + * Field on the Store holding registration data. + * + * @var string + */ + protected $registration; + /** * Constructs a new TaxZone instance. * @@ -72,6 +80,9 @@ public function __construct(array $definition) { foreach ($definition['rates'] as $rate_definition) { $this->rates[] = new TaxRate($rate_definition); } + if (isset($definition['registration'])) { + $this->registration = $definition['registration']; + } } /** @@ -144,4 +155,17 @@ public function match(AddressInterface $address) { return FALSE; } + /** + * Checks if the zone is registered if a registration field is specificed. + * + * @return bool + * Whether the zone is registered. + */ + public function isRegistered(StoreInterface $store) { + if (isset($this->registration)) { + return (bool) $store->get($this->registration)->value; + } + return TRUE; + } + }