diff --git a/uc_cart/src/Form/CartForm.php b/uc_cart/src/Form/CartForm.php index f6cf63e..9d1a222 100644 --- a/uc_cart/src/Form/CartForm.php +++ b/uc_cart/src/Form/CartForm.php @@ -118,7 +118,10 @@ class CartForm extends FormBase { $i = 0; $subtotal = 0; foreach ($cart->getContents() as $cart_item) { + // Prepare the CartItem entity to be displayed in the cart form. $item = $this->moduleHandler->invoke($cart_item->data->module, 'uc_cart_display', [$cart_item]); + // Allow other modules (e.g. Tax) to alter the cart item before display. + \Drupal::moduleHandler()->alter('uc_cart_display', $item, $cart_item); if (Element::children($item)) { $form['items'][$i]['remove'] = $item['remove']; $form['items'][$i]['remove']['#name'] = 'remove-' . $i; diff --git a/uc_tax/config/install/uc_tax.settings b/uc_tax/config/install/uc_tax.settings.yml similarity index 100% rename from ubercart/uc_tax/config/install/uc_tax.settings rename to ubercart_modified/uc_tax/config/install/uc_tax.settings.yml diff --git a/uc_tax/src/Plugin/TaxRatePluginManager.php b/uc_tax/src/Plugin/TaxRatePluginManager.php index c5534d5..b3ac5c7 100644 --- a/uc_tax/src/Plugin/TaxRatePluginManager.php +++ b/uc_tax/src/Plugin/TaxRatePluginManager.php @@ -7,12 +7,20 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\uc_tax\Annotation\UbercartTaxRate; use Drupal\uc_tax\TaxRatePluginInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Manages discovery and instantiation of TaxRate plugins. */ class TaxRatePluginManager extends DefaultPluginManager { + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * Constructs a TaxRatePluginManager object. * @@ -24,11 +32,24 @@ class TaxRatePluginManager extends DefaultPluginManager { * Cache backend instance to use. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler to invoke the alter hook with. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The module handler to invoke the alter hook with. */ - public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_manager) { parent::__construct('Plugin/Ubercart/TaxRate', $namespaces, $module_handler, TaxRatePluginInterface::class, UbercartTaxRate::class); $this->alterInfo('uc_tax_rate'); $this->setCacheBackend($cache_backend, 'uc_tax_rate'); + $this->entityTypeManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = []) { + $instance = parent::createInstance($plugin_id, $configuration); + // Inject EntityTypeManager. + $instance->setEntityManager($this->entityTypeManager); + return $instance; } } diff --git a/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php b/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php index 57a945f..7d06793 100644 --- a/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php +++ b/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php @@ -6,6 +6,8 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\field\Entity\FieldConfig; use Drupal\uc_order\OrderInterface; use Drupal\uc_tax\TaxRatePluginBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\uc_tax\Entity\TaxRate; /** * Defines the fixed percentage tax rate. @@ -35,7 +37,9 @@ class PercentageTaxRate extends TaxRatePluginBase { public function buildConfigurationForm(array $form, FormStateInterface $form_state) { $fields = ['' => $this->t('- None -')]; $result = \Drupal::entityQuery('field_config') - ->condition('field_type', 'number') + ->condition('field_type', + ['integer', 'float', 'decimal', 'list_integer', 'list_decimal'] + ) ->execute(); foreach (FieldConfig::loadMultiple($result) as $field) { $fields[$field->getName()] = $field->label(); @@ -87,23 +91,51 @@ class PercentageTaxRate extends TaxRatePluginBase { /** * {@inheritdoc} */ - public function calculateTax(OrderInterface $order) { + public function calculateProductTax(EntityInterface $item, OrderInterface $order = NULL) { + $node = $this->entityTypeManager->getStorage('node')->load($item->id()); + $price = is_object($item->price) ? $item->price->value : $item->price; $rate = $this->configuration['rate']; - $jurisdiction = $this->configuration['jurisdiction']; $field = $this->configuration['field']; + if (isset($node->$field->value)) { + $product_rate = $node->$field->value; + } + else { + $product_rate = $rate; + } + $calculated_tax = $price * floatval($product_rate) / 100; + return $calculated_tax; + } + + /** + * {@inheritdoc} + */ + public function calculateOrderTax(OrderInterface $order, TaxRate $tax) { + $rate = $this->configuration['rate']; + $field = $this->configuration['field']; + $line_item_types = $tax->getLineItemTypes(); + $product_types = $tax->getProductTypes(); + $calculated_tax = 0; + foreach ($order->products as $product) { - if (isset($product->nid->entity->$field->value)) { - $product_rate = $product->nid->entity->$field->value * $product->qty->value; - } - else { - $product_rate = $this->configuration['rate'] * $product->qty->value; + $node = node_load($product->nid->target_id); + if (in_array($node->bundle(), $product_types)) { + if (isset($node->$field->value)) { + $product_rate = $node->$field->value * $product->qty->value; + } + else { + $product_rate = $rate * $product->qty->value; + } + $calculated_tax += $product->price->value * floatval($product_rate) / 100; } - - $rate += $product->price->value * floatval($product_rate) / 100; } - return [$rate]; + foreach ($order->line_items as $line_item) { + if (in_array($line_item['type'], $line_item_types)) { + $calculated_tax += $line_item['amount'] * floatval($rate) / 100; + } + } + return $calculated_tax; } } diff --git a/uc_tax/src/TaxRateListBuilder.php b/uc_tax/src/TaxRateListBuilder.php index 0956f21..a769083 100644 --- a/uc_tax/src/TaxRateListBuilder.php +++ b/uc_tax/src/TaxRateListBuilder.php @@ -10,11 +10,13 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\uc_tax\Plugin\TaxRatePluginManager; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Messenger\MessengerTrait; /** * Provides a listing of tax rate entities. */ class TaxRateListBuilder extends DraggableListBuilder { + use MessengerTrait; /** * The tax rate plugin manager. diff --git a/uc_tax/src/TaxRatePluginBase.php b/uc_tax/src/TaxRatePluginBase.php index 92b559b..a4031bf 100644 --- a/uc_tax/src/TaxRatePluginBase.php +++ b/uc_tax/src/TaxRatePluginBase.php @@ -5,12 +5,22 @@ namespace Drupal\uc_tax; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\uc_order\OrderInterface; +use Drupal\uc_tax\Entity\TaxRate; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Defines a base tax rate plugin implementation. */ abstract class TaxRatePluginBase extends PluginBase implements TaxRatePluginInterface { + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * {@inheritdoc} */ @@ -20,6 +30,13 @@ abstract class TaxRatePluginBase extends PluginBase implements TaxRatePluginInte $this->configuration += $this->defaultConfiguration(); } + /** + * Set EntityTypeManager. + */ + public function setEntityManager(EntityTypeManagerInterface $entity_manager) { + $this->entityTypeManager = $entity_manager; + } + /** * {@inheritdoc} */ @@ -70,7 +87,14 @@ abstract class TaxRatePluginBase extends PluginBase implements TaxRatePluginInte /** * {@inheritdoc} */ - public function calculateTax(OrderInterface $order) { + public function calculateProductTax(EntityInterface $item, OrderInterface $order = NULL) { + return []; + } + + /** + * {@inheritdoc} + */ + public function calculateOrderTax(OrderInterface $order, TaxRate $tax) { return []; } diff --git a/uc_tax/src/TaxRatePluginInterface.php b/uc_tax/src/TaxRatePluginInterface.php index 93d5929..19e7ac2 100644 --- a/uc_tax/src/TaxRatePluginInterface.php +++ b/uc_tax/src/TaxRatePluginInterface.php @@ -6,6 +6,8 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\uc_order\OrderInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\uc_tax\Entity\TaxRate; /** * Defines an interface for TaxRate plugins. @@ -23,11 +25,27 @@ interface TaxRatePluginInterface extends PluginInspectionInterface, Configurable /** * Returns the amount of tax for the order. * + * @param \Drupal\Core\Entity\EntityInterface $item + * The entity being processed. * @param \Drupal\uc_order\OrderInterface $order * The order that is being processed. * - * @return \Drupal\uc_tax\TaxLineItem + * @return float + * The amount of tax to be added to the product. */ - public function calculateTax(OrderInterface $order); + public function calculateProductTax(EntityInterface $item, OrderInterface $order = NULL); + + /** + * Returns the amount of tax for the order. + * + * @param \Drupal\uc_order\OrderInterface $order + * The order that is being processed. + * @param \Drupal\uc_tax\Entity\TaxRate $tax + * The tax entity. + * + * @return float + * The amount of tax to be added to the order. + */ + public function calculateOrderTax(OrderInterface $order, TaxRate $tax); } diff --git a/uc_tax/tests/src/Functional/InclusiveTaxTest.php b/uc_tax/tests/src/Functional/InclusiveTaxTest.php index 403d7af..1d57aff 100644 --- a/uc_tax/tests/src/Functional/InclusiveTaxTest.php +++ b/uc_tax/tests/src/Functional/InclusiveTaxTest.php @@ -32,17 +32,16 @@ class InclusiveTaxTest extends TaxTestBase { $this->createPaymentMethod('other'); // Create a 20% inclusive tax rate. - $rate = (object) [ - 'name' => $this->randomMachineName(8), - 'rate' => 0.2, - 'taxed_product_types' => ['product'], - 'taxed_line_items' => [], - 'weight' => 0, + $rate = [ + 'label' => $this->randomMachineName(8), + 'settings[rate]' => 20, + 'settings[jurisdiction]' => 'Uberland', 'shippable' => 0, + 'product_types[product]' => 1, 'display_include' => 1, 'inclusion_text' => $this->randomMachineName(6), ]; - uc_tax_rate_save($rate); + $tax_rate = $this->createTaxRate('percentage_rate', $rate); // Ensure Rules picks up the new condition. // entity_flush_caches(); @@ -51,28 +50,22 @@ class InclusiveTaxTest extends TaxTestBase { $product = $this->createProduct(['price' => 10]); // Create an attribute. - $attribute = (object) [ - 'name' => $this->randomMachineName(8), - 'label' => $this->randomMachineName(8), - 'description' => $this->randomMachineName(8), + $attribute = $this->createAttribute([ 'required' => TRUE, 'display' => 1, 'ordering' => 0, - ]; - uc_attribute_save($attribute); + ]); // Create an option with a price adjustment of $5. - $option = (object) [ + $option = $this->createAttributeOption([ 'aid' => $attribute->aid, - 'name' => $this->randomMachineName(8), 'cost' => 0, 'price' => 5, 'weight' => 0, 'ordering' => 0, - ]; - uc_attribute_option_save($option); + ]); - // Attach the attribute to the product. + // Attach the attribute to the product, with option. $attribute = uc_attribute_load($attribute->aid); uc_attribute_subject_save($attribute, 'product', $product->id(), TRUE); @@ -94,7 +87,7 @@ class InclusiveTaxTest extends TaxTestBase { // Ensure the price is displayed tax-inclusively on the node form. // We expect to see $10.80 = $10.00 product - $1.00 kit discount + 20% tax. $this->drupalGet('node/' . $kit->id()); - $assert->pageTextContains('$10.80' . $rate->inclusion_text); + $assert->pageTextContains('$10.80' . $tax_rate->getInclusionText()); // We expect to see $6.00 = $5.00 option adjustment + 20% tax. $assert->responseContains($option->name . ', +$6.00'); @@ -105,23 +98,25 @@ class InclusiveTaxTest extends TaxTestBase { // Check that the subtotal is $16.80 on the cart page. // ($10 base + $5 option - $1 discount, with 20% tax.) $this->drupalGet('cart'); - $this->assertSession()->pageTextMatches('/Subtotal:\s*\$16.80/'); + $assert->pageTextMatches('/Subtotal:\s*\$16.80/'); // Make sure that the subtotal is also correct on the checkout page. $this->drupalPostForm('cart', [], 'Checkout'); // @todo re-enable this test, see [#2306379] - // $assert->pageTextMatches('/Subtotal:\s*\$16.80/'); + $assert->pageTextMatches('/Subtotal:\s*\$16.80/'); // Manually proceed to checkout review. $edit = $this->populateCheckoutForm(); $this->drupalPostForm('cart/checkout', $edit, 'Review order'); - $assert->responseContains('Your order is almost complete.'); + $assert->pageTextContains('Your order is almost complete.'); // Make sure the price is still listed tax-inclusively in cart pane on // the checkout page. // @todo This could be handled more specifically with a regex. // @todo re-enable this test, see [#2306379] - // $assert->pageTextContains('$16.80' . $rate->inclusion_text); + // @todo Suffix not added yet, see uc_tax_preprocess_uc_cart_review_table(). + // $assert->pageTextContains('$16.80' . $tax_rate->getInclusionText()); + $assert->pageTextContains('$16.80'); // Ensure the tax-inclusive price is listed on the order admin view page. $order_ids = \Drupal::entityQuery('uc_order') @@ -131,17 +126,18 @@ class InclusiveTaxTest extends TaxTestBase { $this->assertTrue($order_id, 'Order was created successfully'); $this->drupalGet('admin/store/orders/' . $order_id); // @todo re-enable this test, see [#2306379] - // $assert->pageTextContains('$16.80' . $rate->inclusion_text); + $assert->pageTextContains('$16.80' . $tax_rate->getInclusionText()); + // @todo Tax inclusive values are not shown on the invoice. // And on the invoice. $this->drupalGet('admin/store/orders/' . $order_id . '/invoice'); // @todo re-enable this test, see [#2306379] - // $assert->pageTextContains('$16.80' . $rate->inclusion_text); + // $assert->pageTextContains('$16.80' . $tax_rate->getInclusionText()); // And on the printable invoice. $this->drupalGet('admin/store/orders/' . $order_id . '/invoice'); // @todo re-enable this test, see [#2306379] - // $assert->pageTextContains('$16.80' . $rate->inclusion_text); + // $assert->pageTextContains('$16.80' . $tax_rate->getInclusionText()); } } diff --git a/uc_tax/tests/src/Functional/StoredTaxTest.php b/uc_tax/tests/src/Functional/StoredTaxTest.php index f3ccb57..45df426 100644 --- a/uc_tax/tests/src/Functional/StoredTaxTest.php +++ b/uc_tax/tests/src/Functional/StoredTaxTest.php @@ -27,21 +27,22 @@ class StoredTaxTest extends TaxTestBase { $this->createPaymentMethod('check'); // Create a 20% inclusive tax rate. - $rate = (object) [ - 'name' => $this->randomMachineName(8), - 'rate' => 0.2, - 'taxed_product_types' => ['product'], - 'taxed_line_items' => [], - 'weight' => 0, + $rate = [ + 'label' => $this->randomMachineName(8), + 'settings[rate]' => 20, + 'settings[jurisdiction]' => 'Uberland', 'shippable' => 0, - 'display_include' => 1, + 'product_types[product]' => 1, + 'display_include' => 0, 'inclusion_text' => '', ]; - uc_tax_rate_save($rate); + $tax_rate = $this->createTaxRate('percentage_rate', $rate); + $tax_amount = $tax_rate->getPlugin()->calculateProductTax($this->product); - $this->drupalGet('admin/store/config/taxes'); - $assert->pageTextContains($rate->name); // Check that tax was saved successfully. + $this->drupalGet('admin/store/config/tax'); + $assert->pageTextContains($tax_rate->label()); + $assert->pageTextContains($tax_rate->getRate()); // Check that Rules configuration was create for this tax rate. // $this->drupalGet("admin/store/config/taxes/manage/uc_tax_$rate->id"); @@ -52,21 +53,21 @@ class StoredTaxTest extends TaxTestBase { // Manually step through checkout, because $this->checkout() // doesn't know about taxes. $this->drupalPostForm('cart', [], 'Checkout'); - // Viewed cart page: Billing pane has been displayed. + // Viewed checkout page: Billing pane has been displayed. $assert->pageTextContains('Enter your billing address and information here.'); - // Viewed cart page: Tax line item displayed. - $assert->responseContains($rate->name); - // Viewed cart page: Correct tax amount displayed. - $assert->responseContains(uc_currency_format($rate->rate * $this->product->price->value)); + // Viewed checkout page: Tax line item displayed. + $assert->pageTextContains($tax_rate->label()); + // Viewed checkout page: Correct tax amount displayed. + $assert->pageTextContains(uc_currency_format($tax_amount)); // Submit the checkout page. $edit = $this->populateCheckoutForm(); $this->drupalPostForm('cart/checkout', $edit, 'Review order'); - $assert->responseContains('Your order is almost complete.'); + $assert->pageTextContains('Your order is almost complete.'); // Viewed checkout page: Tax line item displayed. - $assert->responseContains($rate->name); + $assert->pageTextContains($tax_rate->label()); // Viewed checkout page: Correct tax amount displayed. - $assert->responseContains(uc_currency_format($rate->rate * $this->product->price->value)); + $assert->pageTextContains(uc_currency_format($tax_amount)); // Complete the review page. $this->drupalPostForm(NULL, [], 'Submit order'); @@ -79,30 +80,31 @@ class StoredTaxTest extends TaxTestBase { $this->pass('Order ' . $order_id . ' has been created'); $this->drupalGet('admin/store/orders/' . $order_id . '/edit'); - $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'on initial order load'); + $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $tax_amount, 'on initial order load'); $this->drupalPostForm('admin/store/orders/' . $order_id . '/edit', [], 'Save changes'); $assert->pageTextContains('Order changes saved.'); - $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'after saving order'); + $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $tax_amount, 'after saving order'); // Change tax rate and ensure order doesn't change. - $oldrate = $rate->rate; - $rate->rate = 0.1; - $rate = uc_tax_rate_save($rate); + $old_amount = $tax_amount; + $tax_rate->setRate(10); + $tax_rate->save(); + $tax_amount = $tax_rate->getPlugin()->calculateProductTax($this->product); // Save order because tax changes are only updated on save. $this->drupalPostForm('admin/store/orders/' . $order_id . '/edit', [], 'Save changes'); $assert->pageTextContains('Order changes saved.'); - $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after rate change'); + $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $old_amount, 'after rate change'); // Change taxable products and ensure order doesn't change. $class = $this->createProductClass(); - $rate->taxed_product_types = [$class->getEntityTypeId()]; - uc_tax_rate_save($rate); + $tax_rate->setProductTypes([$class->getEntityTypeId()]); + $tax_rate->save(); // entity_flush_caches(); $this->drupalPostForm('admin/store/orders/' . $order_id . '/edit', [], 'Save changes'); $assert->pageTextContains('Order changes saved.'); - $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after applicable product change'); + $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $old_amount, 'after applicable product change'); // Change order Status back to in_checkout and ensure tax-rate // changes now update the order. @@ -112,11 +114,11 @@ class StoredTaxTest extends TaxTestBase { $this->assertFalse($this->loadTaxLine($order_id), 'The tax line was removed from the order when order status changed back to in_checkout.'); // Restore taxable product and ensure new tax is added. - $rate->taxed_product_types = ['product']; - uc_tax_rate_save($rate); + $tax_rate->setProductTypes(['product']); + $tax_rate->save(); $this->drupalPostForm('admin/store/orders/' . $order_id . '/edit', [], 'Save changes'); $assert->pageTextContains('Order changes saved.'); - $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'when order status changed back to in_checkout'); + $this->assertTaxLineCorrect($this->loadTaxLine($order_id), $tax_amount, 'when order status changed back to in_checkout'); } else { $this->fail('No order was created.'); @@ -141,13 +143,13 @@ class StoredTaxTest extends TaxTestBase { /** * Complex assert to check various parts of the tax line item. */ - protected function assertTaxLineCorrect($line, $rate, $when) { + protected function assertTaxLineCorrect($line, $tax_amount, $when) { /** @var \Drupal\Tests\WebAssert $assert */ $assert = $this->assertSession(); $this->assertTrue($line, 'The tax line item was saved to the order ' . $when); - $this->assertTrue(number_format($rate * $this->product->price->value, 2) == number_format($line['amount'], 2), 'Stored tax line item has the correct amount ' . $when); - $this->assertFieldByName('line_items[' . $line['line_item_id'] . '][li_id]', $line['line_item_id'], 'Found the tax line item ID ' . $when); + $this->assertTrue(number_format($tax_amount, 2) == number_format($line['amount'], 2), 'Stored tax line item has the correct amount ' . $when); + $assert->hiddenFieldValueEquals('line_items[' . $line['line_item_id'] . '][li_id]', $line['line_item_id']); $assert->pageTextContains($line['title']); $this->pass('Found the tax title ' . $when); $assert->pageTextContains(uc_currency_format($line['amount'])); diff --git a/uc_tax/tests/src/Functional/TaxRateUiTest.php b/uc_tax/tests/src/Functional/TaxRateUiTest.php index 7ab1e54..8c8c298 100644 --- a/uc_tax/tests/src/Functional/TaxRateUiTest.php +++ b/uc_tax/tests/src/Functional/TaxRateUiTest.php @@ -34,8 +34,6 @@ class TaxRateUiTest extends TaxTestBase { 'shippable' => 0, 'product_types[product]' => 1, 'product_types[blank-line]' => 1, - // No shipping line item if uc_quote not installed. - // 'line_item_types[shipping]' => 1, 'line_item_types[generic]' => 1, 'line_item_types[tax]' => 1, 'display_include' => 1, @@ -45,8 +43,7 @@ class TaxRateUiTest extends TaxTestBase { $this->drupalGet('admin/store/config/tax'); // Verify that tax was saved successfully by checking for expected label, - // rate, taxed shipping types, taxed product types, and taxed line item - // types. + // rate, taxed product types, and taxed line item types. $assert->pageTextContains($tax_rate->label()); $assert->pageTextContains($tax_rate->getRate() . '%'); // Expected shipping types. diff --git a/uc_tax/uc_tax.module b/uc_tax/uc_tax.module index 8794160..1c7fa2a 100644 --- a/uc_tax/uc_tax.module +++ b/uc_tax/uc_tax.module @@ -14,6 +14,7 @@ use Drupal\node\Entity\NodeType; use Drupal\node\NodeInterface; use Drupal\uc_order\OrderInterface; use Drupal\node\Entity\Node; +use Drupal\uc_tax\Entity\TaxRate; /** * Implements hook_module_implements_alter(). @@ -22,7 +23,13 @@ use Drupal\node\Entity\Node; * calculations are made. */ function uc_tax_module_implements_alter(&$implementations, $hook) { - if (in_array($hook, ['uc_order_insert', 'uc_order_update', 'entity_view_alter'])) { + if (in_array($hook, [ + 'uc_order_insert', + 'uc_order_update', + 'entity_view_alter', + 'uc_cart_display_alter', + ])) { + $group = $implementations['uc_tax']; unset($implementations['uc_tax']); $implementations['uc_tax'] = $group; @@ -39,10 +46,10 @@ function uc_tax_form_uc_order_edit_form_alter(&$form, FormStateInterface $form_s // Tax line items are stored in the database, but they can't be changed by // the user. if ($item['type'] == 'tax') { - $form['line_items'][$item['line_item_id']]['title'] = [ + $form['line_items']['line_items'][$item['line_item_id']]['title'] = [ '#markup' => $item['title'], ]; - $form['line_items'][$item['line_item_id']]['amount'] = [ + $form['line_items']['line_items'][$item['line_item_id']]['amount'] = [ '#theme' => 'uc_price', '#price' => $item['amount'], ]; @@ -68,29 +75,17 @@ function uc_tax_uc_product_alter(&$node) { */ function uc_tax_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { switch ($entity->getEntityTypeId()) { - case 'uc_cart_item': - list($amount, $suffixes) = uc_tax_get_included_tax($entity, isset($entity->order) ? $entity->order : NULL); - - if (!empty($amount) && !empty($build['#total'])) { - $build['#total'] += $amount * $build['qty']['#default_value']; - } - - if (!empty($suffixes)) { - if (empty($build['#suffixes'])) { - $build['#suffixes'] = []; - } - $build['#suffixes'] += $suffixes; + // Applies to the order admin view page. + case 'uc_order': + foreach ($entity->products as $product) { + list($amount, $suffixes) = uc_tax_get_included_tax($product, $entity); + + $build['products']['pane']['#rows'][$product->order_product_id->value]['price']['data']['#price'] += $amount; + $build['products']['pane']['#rows'][$product->order_product_id->value]['total']['data']['#price'] += $amount * $product->qty->value; + $build['products']['pane']['#rows'][$product->order_product_id->value]['price']['data']['#suffixes'] += $suffixes; + $build['products']['pane']['#rows'][$product->order_product_id->value]['total']['data']['#suffixes'] += $suffixes; } break; - - case 'uc_order_product': - list($amount, $suffixes) = uc_tax_get_included_tax($entity, isset($entity->order) ? $entity->order : NULL); - - $build['price']['#price'] += $amount; - $build['total']['#price'] += $amount * $entity->qty->value; - $build['price']['#suffixes'] += $suffixes; - $build['total']['#suffixes'] += $suffixes; - break; } } @@ -108,9 +103,10 @@ function uc_tax_uc_order_insert(OrderInterface $order) { */ function uc_tax_uc_order_update(OrderInterface $order) { $changes = []; + $line_items = uc_tax_calculate($order); - foreach ($line_items as $id => $tax) { - $line_items[$id] = _uc_tax_to_line_item($tax); + foreach ($line_items as $tax) { + $line_items[$tax->id] = _uc_tax_to_line_item($tax); } // Loop through existing line items and update or delete as necessary. @@ -162,7 +158,7 @@ function uc_tax_uc_order_update(OrderInterface $order) { */ function uc_tax_node_type_update(EntityInterface $info) { $original_id = $info->getOriginalId(); - $existing_type = !empty($original_id) ? $info->getOriginalId() : $info->getEntityTypeId(); + $existing_type = !empty($original_id) ? $original_id : $info->getEntityTypeId(); db_update('uc_tax_taxed_product_types') ->fields([ @@ -261,7 +257,6 @@ function uc_tax_rate_save($rate, $reset = TRUE) { return $rate; } - /** * List all the taxes that can apply to an order. * @@ -328,6 +323,9 @@ function uc_tax_filter_rates(OrderInterface $order = NULL) { function uc_tax_rate_load($rate_id = NULL) { static $rates = []; + $tax_rates = TaxRate::loadMultiple(); + uasort($tax_rates, 'Drupal\uc_tax\Entity\TaxRate::sort'); + // If the rates have not been cached yet... if (empty($rates)) { // Get all the rate data from the database. @@ -342,12 +340,11 @@ function uc_tax_rate_load($rate_id = NULL) { $rates[$rate->id] = $rate; } + } - foreach (['taxed_product_types', 'taxed_line_items'] as $field) { - $result = db_select('uc_tax_' . $field, 't')->fields('t', ['tax_id', 'type'])->execute(); - foreach ($result as $record) { - $rates[$record->tax_id]->{$field}[] = $record->type; - } + foreach ($tax_rates as $tax_rate) { + if ($tax_rate->status()) { + $rates[$tax_rate->id()] = $tax_rate; } } @@ -434,7 +431,7 @@ function uc_tax_uc_calculate_tax(OrderInterface $order) { /** * Calculates taxable amount for a single product. */ -function uc_tax_apply_item_tax($item, $tax) { +function uc_tax_apply_item_tax($item, $tax, $order = NULL) { // @todo The $item parameter can be many different objects, refactor this! $nid = $item instanceof NodeInterface ? $item->id() : $item->nid->target_id; @@ -469,11 +466,11 @@ function uc_tax_apply_item_tax($item, $tax) { // Tax products if they are of a taxed type and if it is shippable if // the tax only applies to shippable products. - if (in_array($type, $tax->taxed_product_types) && ($tax->shippable == 0 || $shippable == 1)) { + if (in_array($type, $tax->getProductTypes()) && ($tax->isForShippable() == 0 || $shippable)) { return is_object($item->price) ? $item->price->value : $item->price; } else { - return FALSE; + return 0; } } @@ -495,7 +492,7 @@ function uc_tax_apply_tax(OrderInterface $order, $tax) { $taxable_amount += $item->qty->value * uc_tax_apply_item_tax($item, $tax); } } - $taxed_line_items = $tax->taxed_line_items; + $taxed_line_items = $tax->getLineItemTypes(); if (is_array($order->line_items) && is_array($taxed_line_items)) { foreach ($order->line_items as $line_item) { if ($line_item['type'] == 'tax') { @@ -513,20 +510,21 @@ function uc_tax_apply_tax(OrderInterface $order, $tax) { $taxable_amount += $other_tax->amount; } } - $amount = $taxable_amount * $tax->rate; + $amount = $tax->getPlugin()->calculateOrderTax($order, $tax); if ($amount) { $line_item = (object) [ - 'id' => $tax->id, - 'name' => $tax->name, + 'id' => $tax->id(), + 'name' => $tax->label(), 'amount' => $amount, - 'weight' => $tax->weight, + 'weight' => $tax->getWeight(), 'summed' => 1, ]; + $tax_settings = $tax->get('settings'); $line_item->data = [ - 'tax_rate' => $tax->rate, + 'tax_rate' => $tax->getRate(), 'tax' => $tax, 'taxable_amount' => $taxable_amount, - 'tax_jurisdiction' => $tax->name, + 'tax_jurisdiction' => isset($tax_settings['jurisdiction']) ? $tax_settings['jurisdiction'] : '', ]; return $line_item; } @@ -548,14 +546,53 @@ function uc_tax_get_included_tax($product, OrderInterface $order = NULL) { $amount = 0; $suffixes = []; foreach (uc_tax_filter_rates($order) as $tax) { - if ($tax->display_include) { - $taxable = uc_tax_apply_item_tax($product, $tax); + if ($tax->isIncludedInPrice()) { + $taxable = uc_tax_apply_item_tax($product, $tax, $order); if (!empty($taxable)) { - $amount += $taxable * $tax->rate; - $suffixes[$tax->inclusion_text] = $tax->inclusion_text; + $amount += $tax->getPlugin()->calculateProductTax($product, $order); + $suffixes[$tax->getInclusionText()] = $tax->getInclusionText(); } } } - return [$amount, $suffixes]; } + +/** + * Implements hook_uc_cart_display_alter(). + */ +function uc_tax_uc_cart_display_alter(&$item, $cart_item) { + list($amount, $suffixes) = uc_tax_get_included_tax($cart_item, isset($cart_item->order) ? $cart_item->order : NULL); + // Product kit items are already handled by uc_tax_uc_product_alter(). + if ($item['module']['#value'] != 'uc_product_kit' && !empty($amount)) { + $item['#total'] += $amount * $cart_item->qty->value; + } + if (!empty($suffixes)) { + $item['#suffixes'] += $suffixes; + } +} + +/** + * Implements hook_preprocess_HOOK(). + */ +function uc_tax_preprocess_uc_cart_review_table(&$variables) { + + foreach ($variables['items'] as $key => $item) { + list($amount, $suffixes) = uc_tax_get_included_tax($item, $item->order_id->entity); + if (!empty($amount)) { + $variables['items'][$key]->price->value += $amount; + } + if (!empty($suffixes)) { + // @todo: not sure sure how to add suffixes here? + } + } +} + +/** + * Implements hook_uc_line_item_alter(). + */ +function uc_tax_uc_line_item_alter(array &$items) { + // Increase weight of Order total so it appears below tax. + if (!empty($items['total'])) { + $items['total']['weight'] = \Drupal::config('uc_tax.settings')->get('tax_line_item.weight') + 2; + } +} diff --git a_modified/uc_tax/uc_tax.module.diff b/uc_tax/uc_tax.module.diff new file mode 100644 index 0000000..4aa6b7d --- /dev/null +++ b/uc_tax/uc_tax.module.diff @@ -0,0 +1,245 @@ +--- ../../../patches/ubercart/uc_tax/uc_tax.module 2019-04-30 12:35:12.853489861 +1200 ++++ ./uc_tax.module 2019-05-24 11:56:28.203487862 +1200 +@@ -14,6 +14,7 @@ + use Drupal\node\NodeInterface; + use Drupal\uc_order\OrderInterface; + use Drupal\node\Entity\Node; ++use Drupal\uc_tax\Entity\TaxRate; + + /** + * Implements hook_module_implements_alter(). +@@ -22,7 +23,13 @@ + * calculations are made. + */ + function uc_tax_module_implements_alter(&$implementations, $hook) { +- if (in_array($hook, ['uc_order_insert', 'uc_order_update', 'entity_view_alter'])) { ++ if (in_array($hook, [ ++ 'uc_order_insert', ++ 'uc_order_update', ++ 'entity_view_alter', ++ 'uc_cart_display_alter', ++ ])) { ++ + $group = $implementations['uc_tax']; + unset($implementations['uc_tax']); + $implementations['uc_tax'] = $group; +@@ -39,10 +46,10 @@ + // Tax line items are stored in the database, but they can't be changed by + // the user. + if ($item['type'] == 'tax') { +- $form['line_items'][$item['line_item_id']]['title'] = [ ++ $form['line_items']['line_items'][$item['line_item_id']]['title'] = [ + '#markup' => $item['title'], + ]; +- $form['line_items'][$item['line_item_id']]['amount'] = [ ++ $form['line_items']['line_items'][$item['line_item_id']]['amount'] = [ + '#theme' => 'uc_price', + '#price' => $item['amount'], + ]; +@@ -68,29 +75,17 @@ + */ + function uc_tax_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + switch ($entity->getEntityTypeId()) { +- case 'uc_cart_item': +- list($amount, $suffixes) = uc_tax_get_included_tax($entity, isset($entity->order) ? $entity->order : NULL); +- +- if (!empty($amount) && !empty($build['#total'])) { +- $build['#total'] += $amount * $build['qty']['#default_value']; +- } +- +- if (!empty($suffixes)) { +- if (empty($build['#suffixes'])) { +- $build['#suffixes'] = []; +- } +- $build['#suffixes'] += $suffixes; ++ // Applies to the order admin view page. ++ case 'uc_order': ++ foreach ($entity->products as $product) { ++ list($amount, $suffixes) = uc_tax_get_included_tax($product, $entity); ++ ++ $build['products']['pane']['#rows'][$product->order_product_id->value]['price']['data']['#price'] += $amount; ++ $build['products']['pane']['#rows'][$product->order_product_id->value]['total']['data']['#price'] += $amount * $product->qty->value; ++ $build['products']['pane']['#rows'][$product->order_product_id->value]['price']['data']['#suffixes'] += $suffixes; ++ $build['products']['pane']['#rows'][$product->order_product_id->value]['total']['data']['#suffixes'] += $suffixes; + } + break; +- +- case 'uc_order_product': +- list($amount, $suffixes) = uc_tax_get_included_tax($entity, isset($entity->order) ? $entity->order : NULL); +- +- $build['price']['#price'] += $amount; +- $build['total']['#price'] += $amount * $entity->qty->value; +- $build['price']['#suffixes'] += $suffixes; +- $build['total']['#suffixes'] += $suffixes; +- break; + } + } + +@@ -108,9 +103,10 @@ + */ + function uc_tax_uc_order_update(OrderInterface $order) { + $changes = []; ++ + $line_items = uc_tax_calculate($order); +- foreach ($line_items as $id => $tax) { +- $line_items[$id] = _uc_tax_to_line_item($tax); ++ foreach ($line_items as $tax) { ++ $line_items[$tax->id] = _uc_tax_to_line_item($tax); + } + + // Loop through existing line items and update or delete as necessary. +@@ -162,7 +158,7 @@ + */ + function uc_tax_node_type_update(EntityInterface $info) { + $original_id = $info->getOriginalId(); +- $existing_type = !empty($original_id) ? $info->getOriginalId() : $info->getEntityTypeId(); ++ $existing_type = !empty($original_id) ? $original_id : $info->getEntityTypeId(); + + db_update('uc_tax_taxed_product_types') + ->fields([ +@@ -328,6 +324,9 @@ + function uc_tax_rate_load($rate_id = NULL) { + static $rates = []; + ++ $tax_rates = TaxRate::loadMultiple(); ++ uasort($tax_rates, 'Drupal\uc_tax\Entity\TaxRate::sort'); ++ + // If the rates have not been cached yet... + if (empty($rates)) { + // Get all the rate data from the database. +@@ -342,12 +341,11 @@ + + $rates[$rate->id] = $rate; + } ++ } + +- foreach (['taxed_product_types', 'taxed_line_items'] as $field) { +- $result = db_select('uc_tax_' . $field, 't')->fields('t', ['tax_id', 'type'])->execute(); +- foreach ($result as $record) { +- $rates[$record->tax_id]->{$field}[] = $record->type; +- } ++ foreach ($tax_rates as $tax_rate) { ++ if ($tax_rate->status()) { ++ $rates[$tax_rate->id()] = $tax_rate; + } + } + +@@ -434,7 +432,7 @@ + /** + * Calculates taxable amount for a single product. + */ +-function uc_tax_apply_item_tax($item, $tax) { ++function uc_tax_apply_item_tax($item, $tax, $order = NULL) { + // @todo The $item parameter can be many different objects, refactor this! + $nid = $item instanceof NodeInterface ? $item->id() : $item->nid->target_id; + +@@ -469,11 +467,11 @@ + + // Tax products if they are of a taxed type and if it is shippable if + // the tax only applies to shippable products. +- if (in_array($type, $tax->taxed_product_types) && ($tax->shippable == 0 || $shippable == 1)) { ++ if (in_array($type, $tax->getProductTypes()) && ($tax->isForShippable() == 0 || $shippable)) { + return is_object($item->price) ? $item->price->value : $item->price; + } + else { +- return FALSE; ++ return 0; + } + } + +@@ -495,7 +493,7 @@ + $taxable_amount += $item->qty->value * uc_tax_apply_item_tax($item, $tax); + } + } +- $taxed_line_items = $tax->taxed_line_items; ++ $taxed_line_items = $tax->getLineItemTypes(); + if (is_array($order->line_items) && is_array($taxed_line_items)) { + foreach ($order->line_items as $line_item) { + if ($line_item['type'] == 'tax') { +@@ -513,20 +511,21 @@ + $taxable_amount += $other_tax->amount; + } + } +- $amount = $taxable_amount * $tax->rate; ++ $amount = $tax->getPlugin()->calculateOrderTax($order, $tax); + if ($amount) { + $line_item = (object) [ +- 'id' => $tax->id, +- 'name' => $tax->name, ++ 'id' => $tax->id(), ++ 'name' => $tax->label(), + 'amount' => $amount, +- 'weight' => $tax->weight, ++ 'weight' => $tax->getWeight(), + 'summed' => 1, + ]; ++ $tax_settings = $tax->get('settings'); + $line_item->data = [ +- 'tax_rate' => $tax->rate, ++ 'tax_rate' => $tax->getRate(), + 'tax' => $tax, + 'taxable_amount' => $taxable_amount, +- 'tax_jurisdiction' => $tax->name, ++ 'tax_jurisdiction' => isset($tax_settings['jurisdiction']) ? $tax_settings['jurisdiction'] : '', + ]; + return $line_item; + } +@@ -548,14 +547,53 @@ + $amount = 0; + $suffixes = []; + foreach (uc_tax_filter_rates($order) as $tax) { +- if ($tax->display_include) { +- $taxable = uc_tax_apply_item_tax($product, $tax); ++ if ($tax->isIncludedInPrice()) { ++ $taxable = uc_tax_apply_item_tax($product, $tax, $order); + if (!empty($taxable)) { +- $amount += $taxable * $tax->rate; +- $suffixes[$tax->inclusion_text] = $tax->inclusion_text; ++ $amount += $tax->getPlugin()->calculateProductTax($product, $order); ++ $suffixes[$tax->getInclusionText()] = $tax->getInclusionText(); + } + } + } +- + return [$amount, $suffixes]; + } ++ ++/** ++ * Implements hook_uc_cart_display_alter(). ++ */ ++function uc_tax_uc_cart_display_alter(&$item, $cart_item) { ++ list($amount, $suffixes) = uc_tax_get_included_tax($cart_item, isset($cart_item->order) ? $cart_item->order : NULL); ++ // Product kit items are already handled by uc_tax_uc_product_alter(). ++ if ($item['module']['#value'] != 'uc_product_kit' && !empty($amount)) { ++ $item['#total'] += $amount * $cart_item->qty->value; ++ } ++ if (!empty($suffixes)) { ++ $item['#suffixes'] += $suffixes; ++ } ++} ++ ++/** ++ * Implements hook_preprocess_HOOK(). ++ */ ++function uc_tax_preprocess_uc_cart_review_table(&$variables) { ++ ++ foreach ($variables['items'] as $key => $item) { ++ list($amount, $suffixes) = uc_tax_get_included_tax($item, $item->order_id->entity); ++ if (!empty($amount)) { ++ $variables['items'][$key]->price->value += $amount; ++ } ++ if (!empty($suffixes)) { ++ // @todo: not sure sure how to add suffixes here? ++ } ++ } ++} ++ ++/** ++ * Implements hook_uc_line_item_alter(). ++ */ ++function uc_tax_uc_line_item_alter(array &$items) { ++ // Increase the weight of the Order total line item so it always shows below tax line items. ++ if (!empty($items['total'])) { ++ $items['total']['weight'] = \Drupal::config('uc_tax.settings')->get('tax_line_item.weight') + 2; ++ } ++} diff --git a/uc_tax/uc_tax.services.yml b/uc_tax/uc_tax.services.yml index 17a6fa6..7936fc1 100644 --- a/uc_tax/uc_tax.services.yml +++ b/uc_tax/uc_tax.services.yml @@ -2,3 +2,4 @@ services: plugin.manager.uc_tax.rate: class: Drupal\uc_tax\Plugin\TaxRatePluginManager parent: default_plugin_manager + arguments: ['@entity.manager']