366 lines
15 KiB
PHP
366 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* NOTICE OF LICENSE
|
|
*
|
|
* This source file is subject to the Open Software License version 3.0
|
|
* that is bundled with this package in the file LICENSE.md
|
|
* It is also available through the world-wide-web at this URL:
|
|
* https://opensource.org/license/osl-3-0-php
|
|
* If you did not receive a copy of the license and are unable to
|
|
* obtain it through the world-wide-web, please send an email
|
|
* to support@qloapps.com so we can send you a copy immediately.
|
|
*
|
|
* DISCLAIMER
|
|
*
|
|
* Do not edit or add to this file if you wish to upgrade this module to a newer
|
|
* versions in the future. If you wish to customize this module for your needs
|
|
* please refer to https://store.webkul.com/customisation-guidelines for more information.
|
|
*
|
|
* @author Webkul IN
|
|
* @copyright Since 2010 Webkul
|
|
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
|
|
*/
|
|
|
|
class TaxConfigurationCore extends ObjectModel
|
|
{
|
|
const DAY_KEYS = array('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun');
|
|
|
|
const CALCULATION_TYPE_FIXED = 0;
|
|
const CALCULATION_TYPE_PERCENTAGE = 1;
|
|
|
|
public $id_tax;
|
|
public $tax_value;
|
|
public $calculation_type;
|
|
public $per_night = 1;
|
|
public $per_person;
|
|
public $has_tiered_pricing;
|
|
public $apply_on_child;
|
|
public $has_child_age_range;
|
|
public $child_calculation_type;
|
|
public $has_multiple_valid_ranges;
|
|
public $special_days;
|
|
|
|
public static $definition = array(
|
|
'table' => 'tax_configuration',
|
|
'primary' => 'id_tax',
|
|
'fields' => array(
|
|
'tax_value' => array('type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat'),
|
|
'calculation_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'),
|
|
'per_night' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'per_person' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'has_tiered_pricing' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'apply_on_child' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'has_child_age_range' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'child_calculation_type' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'),
|
|
'has_multiple_valid_ranges' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
|
|
'special_days' => array('type' => self::TYPE_STRING),
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Override add() because id_tax is a FK-as-PK — the base ObjectModel::add() never writes the primary key column, which would default to 0 and create phantom rows.
|
|
*
|
|
* @param bool $auto_date ignored — table has no date_add/date_upd
|
|
* @param bool $null_values ignored
|
|
* @return bool
|
|
*/
|
|
public function add($auto_date = true, $null_values = false)
|
|
{
|
|
$idTax = (int) $this->id;
|
|
if (!$idTax) {
|
|
return false;
|
|
}
|
|
$result = Db::getInstance()->insert(
|
|
'tax_configuration',
|
|
array(
|
|
'id_tax' => $idTax,
|
|
'tax_value' => (float) $this->tax_value,
|
|
'calculation_type' => (int) $this->calculation_type,
|
|
'per_night' => (int) $this->per_night,
|
|
'per_person' => (int) $this->per_person,
|
|
'has_tiered_pricing' => (int) $this->has_tiered_pricing,
|
|
'apply_on_child' => (int) $this->apply_on_child,
|
|
'has_child_age_range' => (int) $this->has_child_age_range,
|
|
'child_calculation_type' => (int) $this->child_calculation_type,
|
|
'has_multiple_valid_ranges' => (int) $this->has_multiple_valid_ranges,
|
|
'special_days' => $this->special_days ? pSQL($this->special_days) : null,
|
|
)
|
|
);
|
|
if ($result) {
|
|
$this->id_tax = $idTax;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Load a tourism tax subtype row by id_tax, cached per request.
|
|
*
|
|
* @param int $idTax
|
|
* @return TaxConfiguration|false
|
|
*/
|
|
public static function getByTaxId($idTax)
|
|
{
|
|
$idTax = (int) $idTax;
|
|
$cacheId = 'TaxConfiguration::getByTaxId-' . $idTax;
|
|
if (Cache::isStored($cacheId)) {
|
|
return Cache::retrieve($cacheId);
|
|
}
|
|
|
|
$obj = new TaxConfiguration($idTax);
|
|
$result = Validate::isLoadedObject($obj) ? $obj : false;
|
|
Cache::store($cacheId, $result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Shared core for cleanFromTourismTaxRulesGroups()/cleanFromVatTaxRulesGroups().
|
|
*
|
|
* @param int $idTax
|
|
* @param bool $isTourismTaxRulesGroup true = clean from tourism tax rules groups, false = clean from VAT tax rules groups
|
|
* @return int
|
|
*/
|
|
protected static function cleanTaxRulesGroupsByTourismFlag($idTax, $isTourismTaxRulesGroup)
|
|
{
|
|
$flag = $isTourismTaxRulesGroup ? 1 : 0;
|
|
$db = Db::getInstance();
|
|
$db->execute(
|
|
'DELETE tr FROM `' . _DB_PREFIX_ . 'tax_rule` tr
|
|
INNER JOIN `' . _DB_PREFIX_ . 'tax_rules_group` trg
|
|
ON trg.`id_tax_rules_group` = tr.`id_tax_rules_group`
|
|
WHERE tr.`id_tax` = ' . (int) $idTax . '
|
|
AND trg.`is_tourism_tax_rule_group` = ' . $flag
|
|
);
|
|
return $db->Affected_Rows();
|
|
}
|
|
|
|
/**
|
|
* Remove this tax from all tourism tax rules groups.
|
|
*
|
|
* @param int $idTax
|
|
* @return int
|
|
*/
|
|
public static function cleanFromTourismTaxRulesGroups($idTax)
|
|
{
|
|
return self::cleanTaxRulesGroupsByTourismFlag($idTax, true);
|
|
}
|
|
|
|
/**
|
|
* Remove this tax from all standard (non-tourism) tax rules groups — symmetric counterpart of cleanFromTourismTaxRulesGroups().
|
|
*
|
|
* @param int $idTax
|
|
* @return int
|
|
*/
|
|
public static function cleanFromVatTaxRulesGroups($idTax)
|
|
{
|
|
return self::cleanTaxRulesGroupsByTourismFlag($idTax, false);
|
|
}
|
|
|
|
/**
|
|
* Format a tax rate/amount for display, VAT as a percentage and tourism tax per its own calc type.
|
|
*
|
|
* @param mixed $value ps_tax.rate (used for non-tourism rows)
|
|
* @param array $row List row — must include is_tourism_tax, tourism_tax_calc_type, tourism_tax_value
|
|
* @param Currency $currency
|
|
* @return string
|
|
*/
|
|
public static function getFormattedRateForDisplay($value, $row, $currency)
|
|
{
|
|
if (!(int) $row['is_tourism_tax']) {
|
|
return Tools::safeOutput($value) . '%';
|
|
}
|
|
$displayValue = Tools::safeOutput((float) $row['tourism_tax_value']);
|
|
if ((int) $row['tourism_tax_calc_type'] === self::CALCULATION_TYPE_PERCENTAGE) {
|
|
return $displayValue . '%';
|
|
}
|
|
$currencySign = Tools::safeOutput(trim($currency->prefix . $currency->suffix));
|
|
return $currencySign . ' ' . $displayValue;
|
|
}
|
|
|
|
/**
|
|
* Delete the subtype row and all related tiers/child-ranges.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function delete()
|
|
{
|
|
if (parent::delete()) {
|
|
$idTax = (int) $this->id;
|
|
Db::getInstance()->execute(
|
|
'DELETE FROM `' . _DB_PREFIX_ . 'tax_price_tier`
|
|
WHERE `id_tax` = ' . $idTax
|
|
);
|
|
Db::getInstance()->execute(
|
|
'DELETE FROM `' . _DB_PREFIX_ . 'tax_child_range`
|
|
WHERE `id_tax` = ' . $idTax
|
|
);
|
|
Db::getInstance()->execute(
|
|
'DELETE FROM `' . _DB_PREFIX_ . 'tax_validity_range`
|
|
WHERE `id_tax` = ' . $idTax
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether tourism tax should be visually folded into the displayed room/service price.
|
|
*
|
|
* @param float $tourismTax
|
|
* @return bool
|
|
*/
|
|
public static function isGrossedUp($tourismTax)
|
|
{
|
|
return (float) $tourismTax > 0 && (bool) Configuration::get('QLO_TOURISM_TAX_GROSSED_UP');
|
|
}
|
|
|
|
/**
|
|
* Resolve the hotel's own jurisdiction address and current tourism-tax collection type — shared by every cart, order, and preview caller that needs this context.
|
|
*
|
|
* @param int $idHotel 0 = no hotel context (fully standalone product)
|
|
* @param Address $fallbackAddress Used only when the hotel has no address on file
|
|
* @return array ['address' => Address, 'collectionType' => int]
|
|
*/
|
|
public static function resolveHotelAddressAndCollectionType($idHotel, $fallbackAddress)
|
|
{
|
|
$idHotel = (int) $idHotel;
|
|
|
|
$address = null;
|
|
if ($idHotel) {
|
|
$hotelAddress = HotelBranchInformation::getAddress($idHotel);
|
|
if ($hotelAddress && !empty($hotelAddress['id_address'])) {
|
|
$address = new Address((int) $hotelAddress['id_address']);
|
|
}
|
|
}
|
|
if (!$address || !Validate::isLoadedObject($address)) {
|
|
$address = $fallbackAddress;
|
|
}
|
|
|
|
$collectionType = HotelBranchInformation::TAX_COLLECTION_TYPE_ONLINE;
|
|
if ($idHotel && Validate::isLoadedObject($hotelBranch = new HotelBranchInformation($idHotel))) {
|
|
$collectionType = (int) $hotelBranch->tourism_tax_collection_type;
|
|
}
|
|
|
|
return array(
|
|
'address' => $address,
|
|
'collectionType' => $collectionType,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve the jurisdiction address, collection type, and occupancy (check-in/nights/adults/children) a service product line needs before computing its tourism tax.
|
|
*
|
|
* @param int $idHotel 0 = no hotel context (fully standalone product)
|
|
* @param int $idHtlBookingDetail 0 = not attached to an order-side room booking
|
|
* @param Address $fallbackAddress Used only when the hotel has no address on file
|
|
* @param int $idHtlCartBooking 0 = not attached to a cart-side room booking (pre-order preview only —
|
|
* ignored when $idHtlBookingDetail is given)
|
|
* @return array ['address' => Address, 'collectionType' => int, 'checkInDate' => string,
|
|
* 'numNights' => int, 'numAdults' => int, 'childrenAges' => int[]]
|
|
*/
|
|
public static function resolveServiceLineTaxContext($idHotel, $idHtlBookingDetail, $fallbackAddress, $idHtlCartBooking = 0)
|
|
{
|
|
$idHotel = (int) $idHotel;
|
|
$idHtlBookingDetail = (int) $idHtlBookingDetail;
|
|
$idHtlCartBooking = (int) $idHtlCartBooking;
|
|
|
|
$hotelContext = self::resolveHotelAddressAndCollectionType($idHotel, $fallbackAddress);
|
|
$address = $hotelContext['address'];
|
|
$collectionType = $hotelContext['collectionType'];
|
|
|
|
$roomBooking = $idHtlBookingDetail ? new HotelBookingDetail($idHtlBookingDetail) : null;
|
|
$cartBooking = (!$roomBooking && $idHtlCartBooking) ? new HotelCartBookingData($idHtlCartBooking) : null;
|
|
if ($roomBooking && Validate::isLoadedObject($roomBooking)) {
|
|
$checkInDate = $roomBooking->date_from;
|
|
$numNights = max(1, (int) HotelHelper::getNumberOfDays($roomBooking->date_from, $roomBooking->date_to));
|
|
$numAdults = (int) $roomBooking->adults;
|
|
$childrenAges = array();
|
|
if ($roomBooking->child_ages) {
|
|
$decoded = json_decode($roomBooking->child_ages, true);
|
|
if (is_array($decoded)) {
|
|
$childrenAges = array_map('intval', $decoded);
|
|
}
|
|
}
|
|
} elseif ($cartBooking && Validate::isLoadedObject($cartBooking)) {
|
|
$checkInDate = $cartBooking->date_from;
|
|
$numNights = max(1, (int) HotelHelper::getNumberOfDays($cartBooking->date_from, $cartBooking->date_to));
|
|
$numAdults = (int) $cartBooking->adults;
|
|
$childrenAges = !empty($cartBooking->child_ages) ? (array) json_decode($cartBooking->child_ages, true) : array();
|
|
} else {
|
|
$checkInDate = date('Y-m-d');
|
|
$numNights = 1;
|
|
$numAdults = 1;
|
|
$childrenAges = array();
|
|
}
|
|
|
|
return array(
|
|
'address' => $address,
|
|
'collectionType' => $collectionType,
|
|
'checkInDate' => $checkInDate,
|
|
'numNights' => $numNights,
|
|
'numAdults' => $numAdults,
|
|
'childrenAges' => $childrenAges,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Preview parameters for a tax rules group — a deliberate 1-adult/1-night approximation for js/admin/price.js's live price preview.
|
|
*
|
|
* @param int $idTaxRulesGroup
|
|
* @param Address $address
|
|
* @param int $idLang
|
|
* @return array List of ['tax_calc_type','is_tiered','tax_value','tiers']
|
|
*/
|
|
public static function getPreviewParams($idTaxRulesGroup, $address, $idLang, $collectionType = HotelBranchInformation::TAX_COLLECTION_TYPE_ONLINE)
|
|
{
|
|
$idTaxRulesGroup = (int) $idTaxRulesGroup;
|
|
if (!$idTaxRulesGroup || Tax::excludeTaxeOption() || (int) $collectionType === HotelBranchInformation::TAX_COLLECTION_TYPE_AT_HOTEL) {
|
|
return array();
|
|
}
|
|
|
|
$taxCalculator = TaxManagerFactory::getManager($address, $idTaxRulesGroup)->getTaxCalculator();
|
|
$preview = array();
|
|
|
|
$today = new DateTime(date('Y-m-d'));
|
|
$todayDayKey = self::DAY_KEYS[$today->format('N') - 1];
|
|
|
|
foreach ($taxCalculator->taxes as $tax) {
|
|
$tourismTax = self::getByTaxId((int) $tax->id);
|
|
if (!$tourismTax) {
|
|
continue;
|
|
}
|
|
|
|
if ((bool) $tourismTax->has_multiple_valid_ranges
|
|
&& !TaxValidityRange::dateMatchesAnyRange(TaxValidityRange::getByTaxId((int) $tax->id), $today)
|
|
) {
|
|
continue;
|
|
}
|
|
if ($tourismTax->special_days) {
|
|
$specialDays = json_decode($tourismTax->special_days, true);
|
|
if (!is_array($specialDays) || !in_array($todayDayKey, $specialDays)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$tiers = array();
|
|
if ((bool) $tourismTax->has_tiered_pricing) {
|
|
foreach (TaxPriceTier::getByTaxId((int) $tax->id) as $tier) {
|
|
$tiers[] = array(
|
|
'min_amount' => (float) $tier['min_amount'],
|
|
'max_amount' => (float) $tier['max_amount'],
|
|
'tax_value' => (float) $tier['tax_value'],
|
|
);
|
|
}
|
|
}
|
|
$preview[] = array(
|
|
'tax_calc_type' => (int) $tourismTax->calculation_type,
|
|
'is_tiered' => (bool) $tourismTax->has_tiered_pricing,
|
|
'tax_value' => (float) $tourismTax->tax_value,
|
|
'tiers' => $tiers,
|
|
);
|
|
}
|
|
|
|
return $preview;
|
|
}
|
|
}
|