uc_tax.module in Ubercart 8.4
Ubercart Tax module.
Allows tax rules to be set up and applied to orders.
File
uc_tax/uc_tax.moduleView source
<?php
/**
* @file
* Ubercart Tax module.
*
* Allows tax rules to be set up and applied to orders.
*/
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
use Drupal\uc_order\OrderInterface;
use Drupal\node\Entity\Node;
/**
* Implements hook_module_implements_alter().
*
* Ensures that all other line items are added to the order before tax
* calculations are made.
*/
function uc_tax_module_implements_alter(&$implementations, $hook) {
if (in_array($hook, [
'uc_order_insert',
'uc_order_update',
'entity_view_alter',
])) {
$group = $implementations['uc_tax'];
unset($implementations['uc_tax']);
$implementations['uc_tax'] = $group;
}
}
/**
* Implements hook_form_uc_order_edit_form_alter().
*/
function uc_tax_form_uc_order_edit_form_alter(&$form, FormStateInterface $form_state) {
$order = $form['#order'];
$line_items = $order->line_items;
foreach ($line_items as $item) {
// 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'] = [
'#markup' => $item['title'],
];
$form['line_items'][$item['line_item_id']]['amount'] = [
'#theme' => 'uc_price',
'#price' => $item['amount'],
];
}
}
}
/**
* Implements hook_uc_product_alter().
*/
function uc_tax_uc_product_alter(&$node) {
list($amount, $suffixes) = uc_tax_get_included_tax($node);
$node->display_price += $amount;
if (!empty($suffixes)) {
$node->display_price_suffixes += $suffixes;
}
}
/**
* Implements hook_entity_view_alter().
*
* Adds included tax (VAT) to display price of applicable products.
*/
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;
}
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;
}
}
/**
* Implements hook_uc_order_insert().
*/
function uc_tax_uc_order_insert(OrderInterface $order) {
uc_tax_uc_order_update($order);
}
/**
* Implements hook_uc_order_update().
*
* Updates and saves tax line items to the 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);
}
// Loop through existing line items and update or delete as necessary.
if (is_array($order->line_items)) {
foreach ($order->line_items as $i => $line) {
if ($line['type'] == 'tax') {
$delete = TRUE;
foreach ($line_items as $id => $new_line) {
if ($new_line['data']['tax_id'] == $line['data']['tax_id']) {
if ($new_line['amount'] != $line['amount']) {
uc_order_update_line_item($line['line_item_id'], $new_line['title'], $new_line['amount'], $new_line['data']);
$order->line_items[$i]['amount'] = $new_line['amount'];
$order->line_items[$i]['data'] = $new_line['data'];
$changes[] = t('Changed %title to %amount.', [
'%amount' => uc_currency_format($new_line['amount']),
'%title' => $new_line['title'],
]);
}
unset($line_items[$id]);
$delete = FALSE;
break;
}
}
if ($delete) {
uc_order_delete_line_item($line['line_item_id']);
unset($order->line_items[$i]);
$changes[] = t('Removed %title.', [
'%title' => $line['title'],
]);
}
}
}
}
// Now add line items for any remaining new taxes.
if (is_array($line_items)) {
foreach ($line_items as $line) {
$order->line_items[] = uc_order_line_item_add($order
->id(), 'tax', $line['title'], $line['amount'], $line['weight'], $line['data']);
$changes[] = t('Added %amount for %title.', [
'%amount' => uc_currency_format($line['amount']),
'%title' => $line['title'],
]);
}
}
// And log the changes to the order.
if (count($changes)) {
$order
->logChanges($changes);
usort($order->line_items, 'Drupal\\Component\\Utility\\SortArray::sortByWeightElement');
}
}
/**
* Implements hook_node_type_update().
*
* Ensure taxed product type are synchronised if the content type is updated.
*/
function uc_tax_node_type_update(EntityInterface $info) {
$original_id = $info
->getOriginalId();
$existing_type = !empty($original_id) ? $info
->getOriginalId() : $info
->getEntityTypeId();
$connection = \Drupal::database();
$connection
->update('uc_tax_taxed_product_types')
->fields([
'type' => $info
->getEntityTypeId(),
])
->condition('type', $existing_type)
->execute();
}
/**
* Converts a tax object to the format expected by line item callbacks.
*
* @param $tax
* A tax object as returned by hook_uc_tax_calculate().
*
* @return array
* A line item array suitable for returning from line item callbacks.
*/
function _uc_tax_to_line_item($tax) {
$line = [
'id' => $tax->summed ? 'tax' : 'tax_included',
'title' => !empty($tax->name) ? $tax->name : $tax->id,
'amount' => $tax->amount,
'weight' => \Drupal::config('uc_tax.settings')
->get('tax_line_item.weight') + (!empty($tax->weight) ? $tax->weight / 10 : 0),
'data' => isset($tax->data) ? $tax->data : [],
];
$line['data']['tax_id'] = $tax->id;
return $line;
}
/**
* Saves a tax rate to the database.
*
* @param $rate
* The tax rate object to be saved.
* @param bool $reset
* If TRUE, resets the Rules cache after saving. Defaults to TRUE.
*
* @return
* The saved tax rate object including the rate ID for new rates.
*/
function uc_tax_rate_save($rate, $reset = TRUE) {
$connection = \Drupal::database();
$fields = [
'name' => $rate->name,
'rate' => $rate->rate,
'shippable' => $rate->shippable,
'weight' => $rate->weight,
'display_include' => $rate->display_include,
'inclusion_text' => $rate->inclusion_text,
];
if (isset($rate->id)) {
$connection
->merge('uc_tax')
->key('id', $rate->id)
->fields($fields)
->execute();
}
else {
$rate->id = $connection
->insert('uc_tax')
->fields($fields)
->execute();
}
$connection
->delete('uc_tax_taxed_product_types')
->condition('tax_id', $rate->id)
->execute();
$connection
->delete('uc_tax_taxed_line_items')
->condition('tax_id', $rate->id)
->execute();
$p_insert = $connection
->insert('uc_tax_taxed_product_types')
->fields([
'tax_id',
'type',
]);
$l_insert = $connection
->insert('uc_tax_taxed_line_items')
->fields([
'tax_id',
'type',
]);
foreach ($rate->taxed_product_types as $type) {
$p_insert
->values([
'tax_id' => $rate->id,
'type' => $type,
]);
}
foreach ($rate->taxed_line_items as $type) {
$l_insert
->values([
'tax_id' => $rate->id,
'type' => $type,
]);
}
$p_insert
->execute();
$l_insert
->execute();
// if ($reset) {
// // Ensure Rules picks up the new condition.
// entity_flush_caches();
// }
return $rate;
}
/**
* List all the taxes that can apply to an order.
*
* The taxes depend on the order status. For orders which are still in
* checkout, any tax can apply. For orders out of checkout, only taxes
* originally saved as line items can apply.
*
* @param \Drupal\uc_order\OrderInterface $order
* The order that taxes are being calculated for.
*/
function uc_tax_filter_rates(OrderInterface $order = NULL) {
$taxes = [];
// If no order, then just return all rates.
if (empty($order)) {
$taxes = uc_tax_rate_load();
}
elseif ($order
->getStateId() != 'in_checkout') {
if (isset($order->line_items)) {
foreach ($order->line_items as $item) {
if ($item['type'] == 'tax') {
if (!empty($item['data']['tax'])) {
// Use the rate stored in the line-item.
$taxes[] = clone $item['data']['tax'];
}
elseif (!empty($item['data']['tax_id']) && ($tax = uc_tax_rate_load($item['data']['tax_id']))) {
// For old orders that don't have all the tax info, all we can do
// is preserve the rate.
$tax = clone $tax;
if (!empty($item['data']['tax_rate'])) {
$tax->rate = $item['data']['tax_rate'];
}
$taxes[] = $tax;
}
}
}
}
}
else {
foreach (uc_tax_rate_load() as $rate) {
$tax = clone $rate;
// if (rules_invoke_component('uc_tax_' . $tax->id, $order)) {
$taxes[] = $tax;
// }
}
}
return $taxes;
}
/**
* Loads a tax rate or all tax rates from the database.
*
* @param $rate_id
* The ID of the specific rate to load or NULL to return all available rates.
*
* @return
* An object representing the requested tax rate or an array of all tax rates
* keyed by rate ID.
*/
function uc_tax_rate_load($rate_id = NULL) {
static $rates = [];
// If the rates have not been cached yet...
if (empty($rates)) {
// Get all the rate data from the database.
$connection = \Drupal::database();
$result = $connection
->query('SELECT id, name, rate, shippable, weight, display_include, inclusion_text FROM {uc_tax} ORDER BY weight');
// Loop through each returned row.
foreach ($result as $rate) {
$rate->taxed_product_types = [];
$rate->taxed_line_items = [];
// Disabled by default, overridden in config.
$rate->enabled = FALSE;
$rates[$rate->id] = $rate;
}
foreach ([
'taxed_product_types',
'taxed_line_items',
] as $field) {
$result = $connection
->select('uc_tax_' . $field, 't')
->fields('t', [
'tax_id',
'type',
])
->execute();
foreach ($result as $record) {
$rates[$record->tax_id]->{$field}[] = $record->type;
}
}
}
// Return a rate as specified.
if ($rate_id) {
return isset($rates[$rate_id]) ? $rates[$rate_id] : FALSE;
}
else {
return $rates;
}
}
/**
* Deletes a tax rate from the database.
*
* @param $rate_id
* The ID of the tax rate to delete.
*/
function uc_tax_rate_delete($rate_id) {
// Delete the tax rate record.
$connection = \Drupal::database();
$connection
->delete('uc_tax')
->condition('id', $rate_id)
->execute();
$connection
->delete('uc_tax_taxed_product_types')
->condition('tax_id', $rate_id)
->execute();
$connection
->delete('uc_tax_taxed_line_items')
->condition('tax_id', $rate_id)
->execute();
// Delete the associated conditions if they have been saved to the database.
// rules_config_delete(['uc_tax_' . $rate_id]);
}
/**
* Calculates the taxes for an order based on enabled tax modules.
*
* @param \Drupal\uc_order\OrderInterface $order
* The full order object for the order want to calculate taxes for.
*
* @return array
* An array of taxes for the order.
*/
function uc_tax_calculate(OrderInterface $order) {
// Find any taxes specified by enabled modules.
$taxes = \Drupal::moduleHandler()
->invokeAll('uc_calculate_tax', [
$order,
]);
return $taxes;
}
/**
* Calculates the amount and types of taxes that apply to an order.
*
* @param \Drupal\uc_order\OrderInterface $order
* The full order object for the order want to calculate taxes for.
*/
function uc_tax_uc_calculate_tax(OrderInterface $order) {
if (!is_object($order)) {
return [];
}
if (empty($order->delivery_postal_code)) {
$order->delivery_postal_code = $order->billing_postal_code;
}
if (empty($order->delivery_zone)) {
$order->delivery_zone = $order->billing_zone;
}
if (empty($order->delivery_country)) {
$order->delivery_country = $order->billing_country;
}
$order->tax = [];
foreach (uc_tax_filter_rates($order) as $tax) {
if ($line_item = uc_tax_apply_tax($order, $tax)) {
$order->tax[$line_item->id] = $line_item;
}
}
return $order->tax;
}
/**
* Calculates taxable amount for a single product.
*/
function uc_tax_apply_item_tax($item, $tax) {
// @todo The $item parameter can be many different objects, refactor this!
$nid = $item instanceof NodeInterface ? $item
->id() : $item->nid->target_id;
// Determine the product type.
if (is_array($item->data) && isset($item->data['type'])) {
// Saved in the order product data array.
$type = $item->data['type'];
}
elseif (empty($nid)) {
// "Blank-line" product.
$type = 'blank-line';
}
elseif ($node = Node::load($nid)) {
// Use type of current node, if it exists.
$type = $node
->getType();
}
else {
// Default to generic product.
$type = 'product';
}
// Determine whether this is a shippable product.
if (is_array($item->data) && isset($item->data['shippable'])) {
// Saved in the order product data array.
$shippable = $item->data['shippable'];
}
elseif (empty($nid)) {
// "Blank line" product.
$shippable = $item->weight > 0;
}
elseif ($node = Node::load($nid)) {
// Use current node.
$shippable = $node->shippable->value;
}
else {
// Use default for this node type.
$settings = NodeType::load($type)
->getModuleSettings('uc_product');
$shippable = isset($settings['shippable']) ? $settings['shippable'] : TRUE;
}
// 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)) {
return is_object($item->price) ? $item->price->value : $item->price;
}
else {
return FALSE;
}
}
/**
* Applies taxes to an order.
*
* @param \Drupal\uc_order\OrderInterface $order
* The order object being considered.
* @param $tax
* The tax rule calculating the amount.
*
* @return array
* The line item array representing the amount of tax.
*/
function uc_tax_apply_tax(OrderInterface $order, $tax) {
$taxable_amount = 0;
if (is_array($order->products)) {
foreach ($order->products as $item) {
$taxable_amount += $item->qty->value * uc_tax_apply_item_tax($item, $tax);
}
}
$taxed_line_items = $tax->taxed_line_items;
if (is_array($order->line_items) && is_array($taxed_line_items)) {
foreach ($order->line_items as $line_item) {
if ($line_item['type'] == 'tax') {
// Don't tax old taxes.
continue;
}
if (in_array($line_item['type'], $taxed_line_items)) {
$taxable_amount += $line_item['amount'];
}
}
}
if (in_array('tax', $taxed_line_items)) {
// Tax taxes that were just calculated.
foreach ($order->tax as $other_tax) {
$taxable_amount += $other_tax->amount;
}
}
$amount = $taxable_amount * $tax->rate;
if ($amount) {
$line_item = (object) [
'id' => $tax->id,
'name' => $tax->name,
'amount' => $amount,
'weight' => $tax->weight,
'summed' => 1,
];
$line_item->data = [
'tax_rate' => $tax->rate,
'tax' => $tax,
'taxable_amount' => $taxable_amount,
'tax_jurisdiction' => $tax->name,
];
return $line_item;
}
}
/**
* Calculates the taxes that should be included in a product's display price.
*
* @param $product
* The product whose included taxes are to be calculated.
* @param \Drupal\uc_order\OrderInterface $order
* The order object being considered.
*
* @return array
* An array with two items: the taxed amount and any suffixes that should
* be printed after the product price.
*/
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 (!empty($taxable)) {
$amount += $taxable * $tax->rate;
$suffixes[$tax->inclusion_text] = $tax->inclusion_text;
}
}
}
return [
$amount,
$suffixes,
];
}
Functions
Name![]() |
Description |
---|---|
uc_tax_apply_item_tax | Calculates taxable amount for a single product. |
uc_tax_apply_tax | Applies taxes to an order. |
uc_tax_calculate | Calculates the taxes for an order based on enabled tax modules. |
uc_tax_entity_view_alter | Implements hook_entity_view_alter(). |
uc_tax_filter_rates | List all the taxes that can apply to an order. |
uc_tax_form_uc_order_edit_form_alter | Implements hook_form_uc_order_edit_form_alter(). |
uc_tax_get_included_tax | Calculates the taxes that should be included in a product's display price. |
uc_tax_module_implements_alter | Implements hook_module_implements_alter(). |
uc_tax_node_type_update | Implements hook_node_type_update(). |
uc_tax_rate_delete | Deletes a tax rate from the database. |
uc_tax_rate_load | Loads a tax rate or all tax rates from the database. |
uc_tax_rate_save | Saves a tax rate to the database. |
uc_tax_uc_calculate_tax | Calculates the amount and types of taxes that apply to an order. |
uc_tax_uc_order_insert | Implements hook_uc_order_insert(). |
uc_tax_uc_order_update | Implements hook_uc_order_update(). |
uc_tax_uc_product_alter | Implements hook_uc_product_alter(). |
_uc_tax_to_line_item | Converts a tax object to the format expected by line item callbacks. |