diff --git a/uc_tax/config/schema/uc_tax.schema.yml b/uc_tax/config/schema/uc_tax.schema.yml index 970869a..07c2505 100644 --- a/uc_tax/config/schema/uc_tax.schema.yml +++ b/uc_tax/config/schema/uc_tax.schema.yml @@ -4,15 +4,12 @@ uc_tax.rate.*: type: config_entity label: 'Tax rate' mapping: - label: - type: label - label: 'Name' id: type: string label: 'Machine-readable name' - rate: - type: float - label: 'Rate multiplier' + label: + type: label + label: 'Name' weight: type: integer label: 'Relative weight' @@ -40,3 +37,22 @@ uc_tax.rate.*: sequence: type: label label: 'Line item type label' + plugin: + type: string + label: 'Plugin' + settings: + type: tax_rate.settings.[%parent.plugin] + +tax_rate.settings.*: + type: tax_rate + +tax_rate.settings.percentage_rate: + type: mapping + label: 'Tax rate configuration settings' + mapping: + rate: + type: float + label: 'Tax rate (per order rate)' + field: + type: string + label: 'Product field holding override values' diff --git a/uc_tax/src/Annotation/UbercartTaxRate.php b/uc_tax/src/Annotation/UbercartTaxRate.php new file mode 100644 index 0000000..d79baeb --- /dev/null +++ b/uc_tax/src/Annotation/UbercartTaxRate.php @@ -0,0 +1,42 @@ +entityTypeManager()->getStorage('uc_tax_rate')->create(array('plugin' => $plugin_id)); + + return $this->entityFormBuilder()->getForm($entity); + } + + /** + * Clones a tax rate. + * + * @param \Drupal\uc_tax\TaxRateInterface $uc_tax_rate + * The tax rate entity. + */ + public function saveClone(TaxRateInterface $uc_tax_rate) { + $name = $uc_tax_rate->label(); + + // Tweak the name and unset the rate ID. + $cloned_rate = $uc_tax_rate->createDuplicate(); + $cloned_rate->setLabel($this->t('Copy of @name', ['@name' => $name])); + // @todo: Have to check for uniqueness of name first - in case we have + // cloned this rate before ... + $cloned_rate->setId($uc_tax_rate->id() . "_clone"); + + // Save the new rate without clearing the Rules cache. + $cloned_rate->save(); + + // Clone the associated conditions as well. + // if ($conditions = rules_config_load('uc_tax_' . $uc_tax_rate->id())) { + // $conditions->id = NULL; + // $conditions->name = ''; + // $conditions->save('uc_tax_' . $uc_tax_rate->id()); + // } + + // entity_flush_caches(); + + // Display a message and redirect back to the methods page. + drupal_set_message($this->t('Tax rate %name was cloned.', ['%name' => $name])); + + return $this->redirect('entity.uc_tax_rate.collection'); + } + + /** + * Performs an operation on the tax rate entity. + * + * @param \Drupal\uc_tax\TaxRateInterface $uc_tax_rate + * The tax rate entity. + * @param string $op + * The operation to perform, usually 'enable' or 'disable'. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect back to the tax rate listing page. + */ + public function performOperation(TaxRateInterface $uc_tax_rate, $op) { + $uc_tax_rate->$op()->save(); + + if ($op == 'enable') { + drupal_set_message($this->t('The %label tax rate has been enabled.', ['%label' => $uc_tax_rate->label()])); + } + elseif ($op == 'disable') { + drupal_set_message($this->t('The %label tax rate has been disabled.', ['%label' => $uc_tax_rate->label()])); + } + + $url = $uc_tax_rate->toUrl('collection'); + return $this->redirect($url->getRouteName(), $url->getRouteParameters(), $url->getOptions()); + } + +} diff --git a/uc_tax/src/Entity/TaxRate.php b/uc_tax/src/Entity/TaxRate.php index 7cc9e1f..5d1fc87 100644 --- a/uc_tax/src/Entity/TaxRate.php +++ b/uc_tax/src/Entity/TaxRate.php @@ -18,23 +18,40 @@ use Drupal\uc_tax\TaxRateInterface; * label = @Translation("Tax rate"), * handlers = { * "access" = "Drupal\Core\Entity\EntityAccessControlHandler", - * "list_builder" = "Drupal\uc_tax\Controller\TaxRateListBuilder", + * "list_builder" = "Drupal\uc_tax\TaxRateListBuilder", * "form" = { - * "add" = "Drupal\uc_tax\Form\TaxRateAddForm", - * "edit" = "Drupal\uc_tax\Form\TaxRateEditForm", + * "default" = "Drupal\uc_tax\Form\TaxRateForm", * "delete" = "Drupal\uc_tax\Form\TaxRateDeleteForm" * } * }, * entity_keys = { * "id" = "id", - * "label" = "label" + * "label" = "label", + * "status" = "status", + * "weight" = "weight" + * }, + * config_export = { + * "id", + * "label", + * "weight", + * "jurisdiction", + * "shippable", + * "display_include", + * "inclusion_text", + * "product_types", + * "line_item_types", + * "plugin", + * "settings", * }, * config_prefix = "rate", * admin_permission = "administer taxes", * links = { * "edit-form" = "/admin/store/config/tax/{uc_tax_rate}", + * "enable" = "/admin/store/config/tax/{uc_tax_rate}/enable", + * "disable" = "/admin/store/config/tax/{uc_tax_rate}/disable", * "delete-form" = "/admin/store/config/tax/{uc_tax_rate}/delete", - * "clone" = "/admin/store/config/tax/{uc_tax_rate}/clone" + * "clone" = "/admin/store/config/tax/{uc_tax_rate}/clone", + * "collection" = "/admin/store/config/tax" * } * ) */ @@ -101,21 +118,34 @@ class TaxRate extends ConfigEntityBase implements TaxRateInterface { * * @var string[] */ - protected $line_item_types; + protected $line_item_types = []; /** * Product item types subject to this tax rate. * * @var string[] */ - protected $product_types; + protected $product_types = []; + /** + * The tax rate plugin ID. + * + * @var string + */ + protected $plugin; + + /** + * The tax rate plugin settings. + * + * @var array + */ + protected $settings = array(); /** * {@inheritdoc} */ - public function getId() { - return $this->id; + public function getPlugin() { + return \Drupal::service('plugin.manager.uc_tax.rate')->createInstance($this->plugin, $this->settings); } /** diff --git a/uc_tax/src/Form/TaxRateForm.php b/uc_tax/src/Form/TaxRateForm.php new file mode 100644 index 0000000..0bc8f99 --- /dev/null +++ b/uc_tax/src/Form/TaxRateForm.php @@ -0,0 +1,203 @@ +plugin = $this->entity->getPlugin(); + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $definition = $this->plugin->getPluginDefinition(); + $form['type'] = array( + '#type' => 'item', + '#title' => $this->t('Type'), + '#markup' => $definition['label'], + ); + + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $this->entity->label(), + '#description' => $this->t('The tax rate name shown to the customer at checkout.'), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#machine_name' => array( + 'exists' => '\Drupal\uc_tax\Entity\TaxRate::load', + ), + '#disabled' => !$this->entity->isNew(), + ); + + $form['settings'] = $this->plugin->buildConfigurationForm([], $form_state); + $form['settings']['#tree'] = TRUE; + + $form['jurisdiction'] = array( + '#type' => 'textfield', + '#title' => $this->t('Jurisdiction'), + '#description' => $this->t('Administrative label for the taxing authority, used to prepare reports of collected taxes.'), + '#default_value' => $this->entity->getJurisdiction(), + '#required' => FALSE, + ); + + $form['shippable'] = array( + '#type' => 'radios', + '#title' => $this->t('Taxed products'), + '#options' => array( + 0 => $this->t('Apply tax to any product regardless of its shippability.'), + 1 => $this->t('Apply tax to shippable products only.'), + ), + '#default_value' => (int) $this->entity->isForShippable(), + ); + + // TODO: Remove the need for a special case for product kit module. + $options = array(); + foreach (node_type_get_names() as $type => $name) { + if ($type != 'product_kit' && uc_product_is_product($type)) { + $options[$type] = $name; + } + } + $options['blank-line'] = $this->t('"Blank line" product'); + + $form['product_types'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Taxed product types'), + '#description' => $this->t('Apply taxes to the specified product types/classes.'), + '#default_value' => $this->entity->getProductTypes(), + '#options' => $options, + ); + + $options = array(); + foreach (_uc_line_item_list() as $id => $line_item) { + if (!in_array($id, ['subtotal', 'tax_subtotal', 'total', 'tax_display'])) { + $options[$id] = $line_item['title']; + } + } + + $form['line_item_types'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Taxed line items'), + '#description' => $this->t('Adds the checked line item types to the total before applying this tax.'), + '#default_value' => $this->entity->getLineItemTypes(), + '#options' => $options, + ); + + $form['display_include'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Include this tax when displaying product prices.'), + '#default_value' => $this->entity->isIncludedInPrice(), + ); + + $form['inclusion_text'] = array( + '#type' => 'textfield', + '#title' => $this->t('Tax inclusion text'), + '#description' => $this->t('This text will be displayed near the price to indicate that it includes tax.'), + '#default_value' => $this->entity->getInclusionText(), + ); + + return parent::form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $this->plugin->validateConfigurationForm($form['settings'], $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $this->plugin->submitConfigurationForm($form['settings'], $form_state); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + + // Modify submit button text. + $actions['submit']['#value'] = $this->t('Save tax rate'); + // Add a cancel link to take us back to the list builder. + $actions['cancel'] = array( + '#type' => 'link', + '#title' => $this->t('Cancel'), + '#url' => Url::fromRoute('entity.uc_tax_rate.collection'), + ); + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + + // Save machine names of product types and line item types. + $this->entity->setProductTypes(array_filter($form_state->getValue('product_types'))); + $this->entity->setLineItemTypes(array_filter($form_state->getValue('line_item_types'))); + + $status = $this->entity->save(); + + // Create an edit link for the logger message. + $edit_link = Link::fromTextAndUrl($this->t('Edit'), $this->entity->toUrl())->toString(); + + if ($status == SAVED_UPDATED) { + // If we edited an existing entity. + drupal_set_message($this->t('Tax rate %label has been updated.', ['%label' => $this->entity->label()])); + $this->logger('uc_tax')->notice('Tax rate %label has been updated.', ['%label' => $this->entity->label(), 'link' => $edit_link]); + } + else { + // If we created a new entity. + drupal_set_message($this->t('Tax rate %label has been added.', ['%label' => $this->entity->label()])); + $this->logger('uc_tax')->notice('Tax rate %label has been added.', ['%label' => $this->entity->label(), 'link' => $edit_link]); + } + + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + } + +} diff --git a/uc_tax/src/Plugin/TaxRatePluginManager.php b/uc_tax/src/Plugin/TaxRatePluginManager.php new file mode 100644 index 0000000..efb7d61 --- /dev/null +++ b/uc_tax/src/Plugin/TaxRatePluginManager.php @@ -0,0 +1,38 @@ +alterInfo('uc_tax_rate'); + $this->setCacheBackend($cache_backend, 'uc_tax_rate'); + } + +} diff --git a/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php b/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php new file mode 100644 index 0000000..b77cbd9 --- /dev/null +++ b/uc_tax/src/Plugin/Ubercart/TaxRate/PercentageTaxRate.php @@ -0,0 +1,104 @@ + 0, + 'field' => '', + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $fields = ['' => $this->t('- None -')]; + $result = \Drupal::entityQuery('field_config') + ->condition('field_type', 'number') + ->execute(); + foreach (FieldConfig::loadMultiple($result) as $field) { + $fields[$field->getName()] = $field->label(); + } + + $form['rate'] = array( + '#type' => 'number', + '#title' => $this->t('Default tax rate'), + '#min' => 0, + '#step' => 'any', + '#description' => $this->t('The percentage of the item price to add to the shipping cost for an item.'), + '#default_value' => $this->configuration['rate'], + '#field_suffix' => $this->t('% (percent)'), + '#required' => TRUE, + ); + $form['field'] = array( + '#type' => 'select', + '#title' => $this->t('Tax rate override field'), + '#description' => $this->t('Overrides the default percentage tax rate for a product, when the field is attached to a product content type and has a value.'), + '#options' => $fields, + '#default_value' => $this->configuration['field'], + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['rate'] = $form_state->getValue('rate'); + $this->configuration['field'] = $form_state->getValue('field'); + } + + /** + * {@inheritdoc} + */ + public function getSummary() { + return $this->t('Rate: @rate%', ['@rate' => $this->configuration['rate']]); + } + + /** + * {@inheritdoc} + */ + public function calculateTax(OrderInterface $order) { + $rate = $this->configuration['rate']; + $field = $this->configuration['field']; + + 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; + } + + $rate += $product->price->value * floatval($product_rate) / 100; + } + + return [$rate]; + } + +} diff --git a/uc_tax/src/TaxRateInterface.php b/uc_tax/src/TaxRateInterface.php index 54884a0..e056c8f 100644 --- a/uc_tax/src/TaxRateInterface.php +++ b/uc_tax/src/TaxRateInterface.php @@ -9,21 +9,13 @@ namespace Drupal\uc_tax; use Drupal\Core\Config\Entity\ConfigEntityInterface; - /** * Defines a interface for a tax rate configuration entity. */ interface TaxRateInterface extends ConfigEntityInterface { /** - * The tax rate ID. - * - * @return string - */ - public function getId(); - - /** - * The tax rate ID. + * Sets the tax rate ID. * * @param string $id * @@ -32,6 +24,14 @@ interface TaxRateInterface extends ConfigEntityInterface { public function setId($id); /** + * Returns the plugin instance. + * + * @return \Drupal\uc_tax\TaxRatePluginInterface + * The plugin instance for this tax rate. + */ + public function getPlugin(); + + /** * The tax rate label. * * @return string diff --git a/uc_tax/src/TaxRateListBuilder.php b/uc_tax/src/TaxRateListBuilder.php new file mode 100644 index 0000000..fc2e2a7 --- /dev/null +++ b/uc_tax/src/TaxRateListBuilder.php @@ -0,0 +1,218 @@ +taxRatePluginManager = $tax_rate_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('plugin.manager.uc_tax.rate') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'uc_tax_rates_form'; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = array( + 'data' => $this->t('Name'), + ); + $header['description'] = array( + 'data' => $this->t('Description'), + ); + $header['jurisdiction'] = array( + 'data' => $this->t('Jurisdiction'), + ); + $header['shippable'] = array( + 'data' => $this->t('Taxed products'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + $header['product_types'] = array( + 'data' => $this->t('Taxed product types'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + $header['line_item_types'] = array( + 'data' => $this->t('Taxed line items'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ); + $header['status'] = array( + 'data' => $this->t('Status'), + ); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $plugin = $entity->getPlugin(); + $row['label'] = $entity->label(); + $row['description']['#markup'] = $plugin->getSummary(); + $row['jurisdiction']['#markup'] = $entity->getJurisdiction(); + $row['shippable']['#markup'] = $entity->isForShippable() ? $this->t('Shippable products') : $this->t('Any product'); + $row['product_types']['#markup'] = implode(', ', $entity->getProductTypes()); + $row['line_item_types']['#markup'] = implode(', ', $entity->getLineItemTypes()); + $row['status']['#markup'] = $entity->status() ? $this->t('Enabled') : $this->t('Disabled'); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function buildOperations(EntityInterface $entity) { + $build = parent::buildOperations($entity); + $build['#links']['clone'] = array( + 'title' => $this->t('Clone'), + 'url' => Url::fromRoute('entity.uc_tax_rate.clone', ['uc_tax_rate' => $entity->id()]), + 'weight' => 10, // 'edit' is 0, 'delete' is 100 + ); + + uasort($build['#links'], 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + return $build; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $options = array_map(function ($definition) { + return $definition['label']; + }, $this->taxRatePluginManager->getDefinitions()); + uasort($options, 'strnatcasecmp'); + + $form['add'] = array( + '#type' => 'details', + '#title' => $this->t('Add a tax rate'), + '#open' => TRUE, + '#attributes' => array( + 'class' => array('container-inline'), + ), + ); + $form['add']['plugin'] = array( + '#type' => 'select', + '#title' => $this->t('Type'), + '#empty_option' => $this->t('- Choose -'), + '#options' => $options, + ); + $form['add']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Add tax rate'), + '#validate' => array('::validateAddMethod'), + '#submit' => array('::submitAddMethod'), + '#limit_validation_errors' => array(array('plugin')), + ); + + $form = parent::buildForm($form, $form_state); + $form[$this->entitiesKey]['#empty'] = $this->t('No tax rates have been configured yet.'); + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Save configuration'), + '#button_type' => 'primary', + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + drupal_set_message($this->t('The configuration options have been saved.')); + } + + /** + * Form validation handler for adding a new rate. + */ + public function validateAddMethod(array &$form, FormStateInterface $form_state) { + if ($form_state->isValueEmpty('plugin')) { + $form_state->setErrorByName('plugin', $this->t('You must select a tax rate type.')); + } + } + + /** + * Form submission handler for adding a new method. + */ + public function submitAddMethod(array &$form, FormStateInterface $form_state) { + $form_state->setRedirect('entity.uc_tax_rate.add_form', ['plugin_id' => $form_state->getValue('plugin')]); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build['description'] = array( + '#markup' => $this->t("
This is a list of the tax rates currently" + . " defined on your Drupal site.
You may use the 'Add tax rate'" + . " button to add a new rate, or use the widget in the 'Operations'" + . " column to edit, delete, enable/disable, or clone existing tax rates." + . " Rates that are disabled will not be applied at checkout and will not" + . " be included in product prices.
" + . "Taxes are sorted by weight and then applied to the order sequentially." + . " This order is important when taxes need to be applied to other tax line items." + . " To re-order, drag the method to its desired location using the drag icon then save" + . " the configuration using the button at the bottom of the page.
" + ), + ); + $build += parent::render(); + + return $build; + } + +} diff --git a/uc_tax/src/TaxRatePluginBase.php b/uc_tax/src/TaxRatePluginBase.php new file mode 100644 index 0000000..ee212ad --- /dev/null +++ b/uc_tax/src/TaxRatePluginBase.php @@ -0,0 +1,82 @@ +configuration += $this->defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function calculateTax(OrderInterface $order) { + return array(); + } + +} diff --git a/uc_tax/src/TaxRatePluginInterface.php b/uc_tax/src/TaxRatePluginInterface.php new file mode 100644 index 0000000..a6fd1b5 --- /dev/null +++ b/uc_tax/src/TaxRatePluginInterface.php @@ -0,0 +1,39 @@ +