<?php defined ('_JEXEC') or die('Restricted access'); /** * Shipment plugin for general, rules-based shipments, like regular postal services with complex shipping cost structures * * @version $Id$ * @package VirtueMart * @subpackage Plugins - shipment * @copyright Copyright (C) 2004-2012 VirtueMart Team - All rights reserved. * @copyright Copyright (C) 2013 Reinhold Kainhofer, reinhold@kainhofer.com * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.php * VirtueMart is free software. This version may have been modified pursuant * to the GNU General Public License, and as distributed it includes or * is derivative of works licensed under the GNU General Public License or * other free or open source software licenses. * See /administrator/components/com_virtuemart/COPYRIGHT.php for copyright notices and details. * * http://virtuemart.org * @author Reinhold Kainhofer, based on the weight_countries shipping plugin by Valerie Isaksen * */ if (!class_exists ('vmPSPlugin')) { require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); } // Only declare the class once... if (class_exists ('plgVmShipmentRules_Shipping_Base')) { return; } /** Shipping costs according to general rules. * Supported Variables: Weight, ZIP, Amount, Products (1 for each product, even if multiple ordered), Articles * Assignable variables: Shipping, Name */ class plgVmShipmentRules_Shipping_Base extends vmPSPlugin { /** * @param object $subject * @param array $config */ function __construct (& $subject, $config) { parent::__construct ($subject, $config); $this->_loggable = TRUE; $this->_tablepkey = 'id'; $this->_tableId = 'id'; $this->tableFields = array_keys ($this->getTableSQLFields ()); $varsToPush = $this->getVarsToPush (); $this->setConfigParameterable ($this->_configTableFieldName, $varsToPush); } /** * Create the table for this plugin if it does not yet exist. * * @author Valérie Isaksen */ public function getVmPluginCreateTableSQL () { return $this->createTableSQL ('Shipment Rules Table'); } /** * @return array */ function getTableSQLFields () { $SQLfields = array( 'id' => 'int(1) UNSIGNED NOT NULL AUTO_INCREMENT', 'virtuemart_order_id' => 'int(11) UNSIGNED', 'order_number' => 'char(32)', 'virtuemart_shipmentmethod_id' => 'mediumint(1) UNSIGNED', 'shipment_name' => 'varchar(5000)', 'rule_name' => 'varchar(500)', 'order_weight' => 'decimal(10,4)', 'order_articles' => 'int(1)', 'order_products' => 'int(1)', 'shipment_weight_unit' => 'char(3) DEFAULT \'KG\'', 'shipment_cost' => 'decimal(10,2)', 'tax_id' => 'smallint(1)' ); return $SQLfields; } /** * This method is fired when showing the order details in the frontend. * It displays the shipment-specific data. * * @param integer $virtuemart_order_id The order ID * @param integer $virtuemart_shipmentmethod_id The selected shipment method id * @param string $shipment_name Shipment Name * @return mixed Null for shipments that aren't active, text (HTML) otherwise * @author Valérie Isaksen * @author Max Milbers */ public function plgVmOnShowOrderFEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id, &$shipment_name) { $this->onShowOrderFE ($virtuemart_order_id, $virtuemart_shipmentmethod_id, $shipment_name); } /** * This event is fired after the order has been stored; it gets the shipment method- * specific data. * * @param int $order_id The order_id being processed * @param object $cart the cart * @param array $order The actual order saved in the DB * @return mixed Null when this method was not selected, otherwise true * @author Valerie Isaksen */ function plgVmConfirmedOrder (VirtueMartCart $cart, $order) { if (!($method = $this->getVmPluginMethod ($order['details']['BT']->virtuemart_shipmentmethod_id))) { return NULL; // Another method was selected, do nothing } if (!$this->selectedThisElement ($method->shipment_element)) { return FALSE; } $values['virtuemart_order_id'] = $order['details']['BT']->virtuemart_order_id; $values['order_number'] = $order['details']['BT']->order_number; $values['virtuemart_shipmentmethod_id'] = $order['details']['BT']->virtuemart_shipmentmethod_id; $values['shipment_name'] = $this->renderPluginName ($method); $values['rule_name'] = $method->rule_name; $values['order_weight'] = $this->getOrderWeight ($cart, $method->weight_unit); $values['order_articles'] = $this->getOrderArticles ($cart); $values['order_products'] = $this->getOrderProducts ($cart); $values['shipment_weight_unit'] = $method->weight_unit; $values['shipment_cost'] = $method->cost; $values['tax_id'] = $method->tax_id; $this->storePSPluginInternalData ($values); return TRUE; } /** * This method is fired when showing the order details in the backend. * It displays the shipment-specific data. * NOTE, this plugin should NOT be used to display form fields, since it's called outside * a form! Use plgVmOnUpdateOrderBE() instead! * * @param integer $virtuemart_order_id The order ID * @param integer $virtuemart_shipmentmethod_id The order shipment method ID * @param object $_shipInfo Object with the properties 'shipment' and 'name' * @return mixed Null for shipments that aren't active, text (HTML) otherwise * @author Valerie Isaksen */ public function plgVmOnShowOrderBEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id) { if (!($this->selectedThisByMethodId ($virtuemart_shipmentmethod_id))) { return NULL; } $html = $this->getOrderShipmentHtml ($virtuemart_order_id); return $html; } /** * @param $virtuemart_order_id * @return string */ function getOrderShipmentHtml ($virtuemart_order_id) { $db = JFactory::getDBO (); $q = 'SELECT * FROM `' . $this->_tablename . '` ' . 'WHERE `virtuemart_order_id` = ' . $virtuemart_order_id; $db->setQuery ($q); if (!($shipinfo = $db->loadObject ())) { vmWarn (500, $q . " " . $db->getErrorMsg ()); return ''; } if (!class_exists ('CurrencyDisplay')) { require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'currencydisplay.php'); } $currency = CurrencyDisplay::getInstance (); $tax = ShopFunctions::getTaxByID ($shipinfo->tax_id); $taxDisplay = is_array ($tax) ? $tax['calc_value'] . ' ' . $tax['calc_value_mathop'] : $shipinfo->tax_id; $taxDisplay = ($taxDisplay == -1) ? JText::_ ('COM_VIRTUEMART_PRODUCT_TAX_NONE') : $taxDisplay; $html = '<table class="adminlist">' . "\n"; $html .= $this->getHtmlHeaderBE (); $html .= $this->getHtmlRowBE ('RULES_SHIPPING_NAME', $shipinfo->shipment_name); $html .= $this->getHtmlRowBE ('RULES_WEIGHT', $shipinfo->order_weight . ' ' . ShopFunctions::renderWeightUnit ($shipinfo->shipment_weight_unit)); $html .= $this->getHtmlRowBE ('RULES_ARTICLES', $shipinfo->order_articles . '/' . $shipinfo->order_products); $html .= $this->getHtmlRowBE ('RULES_COST', $currency->priceDisplay ($shipinfo->shipment_cost)); $html .= $this->getHtmlRowBE ('RULES_TAX', $taxDisplay); $html .= '</table>' . "\n"; return $html; } /** Include the rule name in the shipment name */ protected function renderPluginName ($plugin) { $return = ''; $plugin_name = $this->_psType . '_name'; $plugin_desc = $this->_psType . '_desc'; $description = ''; // $params = new JParameter($plugin->$plugin_params); // $logo = $params->get($this->_psType . '_logos'); $logosFieldName = $this->_psType . '_logos'; $logos = $plugin->$logosFieldName; if (!empty($logos)) { $return = $this->displayLogos ($logos) . ' '; } if (!empty($plugin->$plugin_desc)) { $description = '<span class="' . $this->_type . '_description">' . $plugin->$plugin_desc . '</span>'; } $rulename=''; if (!empty($plugin->rule_name)) { $rulename=" (".htmlspecialchars($plugin->rule_name).")"; } $pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description; return $pluginName; } /** * @param VirtueMartCart $cart * @param $method * @param $cart_prices * @return int */ function getCosts (VirtueMartCart $cart, $method, $cart_prices) { if (!isset($method->rules)) $this->parseMethodRules($method); $cartvals = $this->getCartValues ($cart, $method, $cart_prices); foreach ($method->rules as $r) { if ($r->matches($cartvals)) { $rulename=$r->getRuleName($cartvals); vmdebug('Rule '.$rulename.' ('.$r->rulestring.') matched.'); $method->tax_id = $r->tax_id; $method->matched_rule = $r; $method->rule_name = $r->getRuleName($cartvals); $method->cost = $r->getShippingCosts($cartvals); $method->includes_tax = $r->includes_tax; return $method->cost; } } vmdebug('getCosts '.$method->name.' does not return shipping costs'); return 0; } /** * update the plugin cart_prices ( * * @author Valérie Isaksen (original), Reinhold Kainhofer (tax calculations from shippingWithTax) * * @param $cart_prices: $cart_prices['salesPricePayment'] and $cart_prices['paymentTax'] updated. Displayed in the cart. * @param $value : fee * @param $tax_id : tax id */ function setCartPrices (VirtueMartCart $cart, &$cart_prices, $method) { if (!class_exists ('calculationHelper')) { require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'calculationh.php'); } $calculator = calculationHelper::getInstance (); $value = $calculator->roundInternal ($this->getCosts ($cart, $method, $cart_prices), 'salesPrice'); $_psType = ucfirst ($this->_psType); $cart_prices[$this->_psType . 'Value'] = $value; $taxrules = array(); if(isset($method->tax_id) and (int)$method->tax_id === -1){ } else if (!empty($method->tax_id)) { $cart_prices[$this->_psType . '_calc_id'] = $method->tax_id; $db = JFactory::getDBO (); $q = 'SELECT * FROM #__virtuemart_calcs WHERE `virtuemart_calc_id`="' . $method->tax_id . '" '; $db->setQuery ($q); $taxrules = $db->loadAssocList (); } else { //This construction makes trouble, if there are products with different vats in the cart //on the other side, it is very unlikely to have different vats in the cart and simultan it is not possible to use a fixed tax rule for the shipment if(!empty($calculator->_cartData['VatTax']) and count ($calculator->_cartData['VatTax']) == 1){ $taxrules = $calculator->_cartData['VatTax']; foreach($taxrules as &$rule){ $rule['subTotal'] = $cart_prices[$this->_psType . 'Value']; } } else { $taxrules = $calculator->_cartData['taxRulesBill']; foreach($taxrules as &$rule){ unset($rule['subTotal']); } } } if (count ($taxrules) > 0) { if ($method->includes_tax) { $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($cart_prices[$this->_psType . 'Value'], 'salesPrice'); // Calculate the tax from the final sales price: $calculator->setRevert (true); $cart_prices[$this->_psType . 'Value'] = $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true)); $cart_prices[$this->_psType . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']; $calculator->setRevert (false); } else { $cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value']), 'salesPrice'); $cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']), 'salesPrice'); } reset($taxrules); $taxrule = current($taxrules); $cart_prices[$this->_psType . '_calc_id'] = $taxrule['virtuemart_calc_id']; } else { $cart_prices['salesPrice' . $_psType] = $value; $cart_prices[$this->_psType . 'Tax'] = 0; $cart_prices[$this->_psType . '_calc_id'] = 0; } return $cart_prices['salesPrice' . $_psType]; } protected function createMethodRule ($r, $countries, $tax) { return new ShippingRule($r, $countries, $tax); } private function parseMethodRule ($rulestring, $countries, $tax, &$method) { $rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring); foreach ($rules1 as $r) { // Ignore empty lines if (empty($r)) continue; $method->rules[] = $this->createMethodRule ($r, $countries, $tax); } } protected function parseMethodRules (&$method) { if (!isset($method->rules)) $method->rules = array(); $this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method); $this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method); $this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method); $this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method); $this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method); $this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method); $this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method); $this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method); } protected function getOrderArticles (VirtueMartCart $cart) { /* Cache the value in a static variable and calculate it only once! */ static $articles = 0; if(empty($articles) and count($cart->products)>0){ foreach ($cart->products as $product) { $articles += $product->quantity; } } return $articles; } protected function getOrderProducts (VirtueMartCart $cart) { /* Cache the value in a static variable and calculate it only once! */ static $products = 0; if(empty($products) and count($cart->products)>0){ $products = count($cart->products); } return $products; } protected function getOrderDimensions (VirtueMartCart $cart, $length_dimension) { /* Cache the value in a static variable and calculate it only once! */ static $calculated = 0; static $dimensions=array( 'volume' => 0, 'maxvolume' => 0, 'minvolume' => 9999999999, 'maxlength' => 0, 'minlength' => 9999999999, 'totallength' => 0, 'maxwidth' => 0, 'minwidth' => 9999999999, 'totalwidth' => 0, 'maxheight' => 0, 'minheight' => 9999999999, 'totalheight' => 0, 'maxpackaging' => 0, 'minpackaging' => 9999999999, 'totalpackaging' => 0, ); if ($calculated==0) { $calculated=1; foreach ($cart->products as $product) { $l = ShopFunctions::convertDimensionUnit ($product->product_length, $product->product_lwh_uom, $length_dimension); $w = ShopFunctions::convertDimensionUnit ($product->product_width, $product->product_lwh_uom, $length_dimension); $h = ShopFunctions::convertDimensionUnit ($product->product_height, $product->product_lwh_uom, $length_dimension); $volume = $l * $w * $h; $dimensions['volume'] += $volume * $product->quantity; $dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume); $dimensions['minvolume'] = min ($dimensions['minvolume'], $volume); $dimensions['totallength'] += $l * $product->quantity; $dimensions['maxlength'] = max ($dimensions['maxlength'], $l); $dimensions['minlength'] = min ($dimensions['minlength'], $l); $dimensions['totalwidth'] += $w * $product->quantity; $dimensions['maxwidth'] = max ($dimensions['maxwidth'], $w); $dimensions['minwidth'] = min ($dimensions['minwidth'], $w); $dimensions['totalheight'] += $h * $product->quantity; $dimensions['maxheight'] = max ($dimensions['maxheight'], $h); $dimensions['minheight'] = min ($dimensions['minheight'], $h); $dimensions['totalpackaging'] += $product->packaging * $product->quantity; $dimensions['maxpackaging'] = max ($dimensions['maxpackaging'], $product->packaging); $dimensions['minpackaging'] = min ($dimensions['minpackaging'], $product->packaging); } } return $dimensions; } function getOrderWeights (VirtueMartCart $cart, $weight_unit) { static $calculated = 0; static $dimensions=array( 'weight' => 0, 'maxweight' => 0, 'minweight' => 9999999999, ); if ($calculated==0 && count($cart->products)>0) { $calculated = 1; foreach ($cart->products as $product) { $w = ShopFunctions::convertWeigthUnit ($product->product_weight, $product->product_weight_uom, $weight_unit); $dimensions['maxweight'] = max ($dimensions['maxweight'], $w); $dimensions['minweight'] = min ($dimensions['minweight'], $w); $dimensions['weight'] += $w * $product->quantity; } } return $dimensions; } function getOrderSKUs (VirtueMartCart $cart) { $skus = array(); foreach ($cart->products as $product) { $skus[] = $product->product_sku; } return $skus; } function getOrderCountryState (VirtueMartCart $cart, $address) { $data = array ( 'countryid' => 0, 'country' => '', 'country2' => '', 'country3' => '', 'stateid' => 0, 'state' => '', 'state2' => '', 'state3' => '', ); $countriesModel = VmModel::getModel('country'); if (isset($address['virtuemart_country_id'])) { $data['countryid'] = $address['virtuemart_country_id']; $countriesModel->setId($address['virtuemart_country_id']); $country = $countriesModel->getData(); if (isset($country)) { $data['country'] = $country->country_name; $data['country2'] = $country->country_2_code; $data['country3'] = $country->country_3_code; } } $statesModel = VmModel::getModel('state'); if (isset($address['virtuemart_state_id'])) { $data['stateid'] = $address['virtuemart_state_id']; $statesModel->setId($address['virtuemart_state_id']); $state = $statesModel->getData(); if (isset($state)) { $data['state'] = $state->state_name; $data['state2'] = $state->state_2_code; $data['state3'] = $state->state_3_code; } } return $data; } /** Allow child classes to add additional variables for the rules */ protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) { } protected function getCartValues (VirtueMartCart $cart, $method, $cart_prices) { $address = (($cart->ST == 0) ? $cart->BT : $cart->ST); $zip = trim($address['zip']); $cartvals = array('zip'=>$zip, 'zip1'=>substr($zip,0,1), 'zip2'=>substr($zip,0,2), 'zip3'=>substr($zip,0,3), 'zip4'=>substr($zip,0,4), 'zip5'=>substr($zip,0,5), 'zip6'=>substr($zip,0,6), 'articles'=>$this->getOrderArticles($cart), 'products'=>$this->getOrderProducts($cart), 'amount'=>$cart_prices['salesPrice'], 'amountwithtax'=>$cart_prices['salesPrice'], 'amountwithouttax'=>$cart_prices['priceWithoutTax'], 'baseprice'=>$cart_prices['basePrice'], 'basepricewithtax'=>$cart_prices['basePriceWithTax'], 'discountedpricewithouttax'=>$cart_prices['discountedPriceWithoutTax'], 'salesprice'=>$cart_prices['salesPrice'], 'taxamount'=>$cart_prices['taxAmount'], 'salespricewithdiscount'=>$cart_prices['salesPriceWithDiscount'], 'discountamount'=>$cart_prices['discountAmount'], 'pricewithouttax'=>$cart_prices['priceWithoutTax'], 'skus'=>$this->getOrderSKUs($cart), // TODO: Add 'categories' variable // TODO: Add 'vendors'variable // 'discountbeforetaxbill'=>$cart_prices['discountBeforeTaxBill'], ); $cartvals = array_merge ($cartvals, $this->getOrderCountryState ($cart, $address)); $cartvals = array_merge ($cartvals, $this->getOrderWeights ($cart, $method->weight_unit)); $cartvals = array_merge ($cartvals, $this->getOrderDimensions ($cart, $method->length_unit)); // Let child classes update the $cartvals array, or add new variables $this->addCustomCartValues($cart, $cart_prices, $cartvals); // JFactory::getApplication()->enqueueMessage("<pre>cart prices: ".print_r($cart_prices,1)."</pre>", 'error'); // JFactory::getApplication()->enqueueMessage("<pre>cart: ".print_r($cart,1)."</pre>", 'error'); // JFactory::getApplication()->enqueueMessage("<pre>cart values: ".print_r($cartvals,1)."</pre>", 'error'); return $cartvals; } /** * @param \VirtueMartCart $cart * @param int $method * @param array $cart_prices * @return bool */ protected function checkConditions ($cart, $method, $cart_prices) { if (!isset($method->rules)) $this->parseMethodRules($method); $cartvals = $this->getCartValues ($cart, $method, $cart_prices); foreach ($method->rules as $r) { if ($r->matches($cartvals)) { $method->matched_rule = $r; $method->rule_name = $r->getRuleName($cartvals); // If NoShipping is set, this method should NOT offer any shipping at all, so return FALSE, otherwise TRUE if ($r->isNoShipping()) { vmdebug('checkConditions '.$method->shipment_name.' indicates NoShipping for rule "'.$method->rule_name.'" ('.$r->rulestring.').'); return FALSE; } else { return TRUE; } } } vmdebug('checkConditions '.$method->shipment_name.' does not fit'); return FALSE; } /** * Create the table for this plugin if it does not yet exist. * This functions checks if the called plugin is active one. * When yes it is calling the standard method to create the tables * * @author Valérie Isaksen * */ function plgVmOnStoreInstallShipmentPluginTable ($jplugin_id) { return $this->onStoreInstallPluginTable ($jplugin_id); } /** * @param VirtueMartCart $cart * @return null */ public function plgVmOnSelectCheckShipment (VirtueMartCart &$cart) { return $this->OnSelectCheck ($cart); } /** * plgVmDisplayListFE * This event is fired to display the pluginmethods in the cart (edit shipment/payment) for example * * @param object $cart Cart object * @param integer $selected ID of the method selected * @return boolean True on success, false on failures, null when this plugin was not selected. * On errors, JError::raiseWarning (or JError::raiseError) must be used to set a message. * * @author Valerie Isaksen * @author Max Milbers */ public function plgVmDisplayListFEShipment (VirtueMartCart $cart, $selected = 0, &$htmlIn) { return $this->displayListFE ($cart, $selected, $htmlIn); } /** * @param VirtueMartCart $cart * @param array $cart_prices * @param $cart_prices_name * @return bool|null */ public function plgVmOnSelectedCalculatePriceShipment (VirtueMartCart $cart, array &$cart_prices, &$cart_prices_name) { return $this->onSelectedCalculatePrice ($cart, $cart_prices, $cart_prices_name); } /** * plgVmOnCheckAutomaticSelected * Checks how many plugins are available. If only one, the user will not have the choice. Enter edit_xxx page * The plugin must check first if it is the correct type * * @author Valerie Isaksen * @param VirtueMartCart cart: the cart object * @return null if no plugin was found, 0 if more then one plugin was found, virtuemart_xxx_id if only one plugin is found * */ function plgVmOnCheckAutomaticSelectedShipment (VirtueMartCart $cart, array $cart_prices = array(), &$shipCounter) { if ($shipCounter > 1) { return 0; } return $this->onCheckAutomaticSelected ($cart, $cart_prices, $shipCounter); } /** * This method is fired when showing when priting an Order * It displays the the payment method-specific data. * * @param integer $_virtuemart_order_id The order ID * @param integer $method_id method used for this order * @return mixed Null when for payment methods that were not selected, text (HTML) otherwise * @author Valerie Isaksen */ function plgVmonShowOrderPrint ($order_number, $method_id) { return $this->onShowOrderPrint ($order_number, $method_id); } function plgVmDeclarePluginParamsShipment ($name, $id, &$data) { return $this->declarePluginParams ('shipment', $name, $id, $data); } /* This function is needed in VM 2.0.14 etc. because otherwise the params are not saved */ function plgVmSetOnTablePluginParamsShipment ($name, $id, &$table) { return $this->setOnTablePluginParams ($name, $id, $table); } function plgVmSetOnTablePluginShipment(&$data,&$table){ $name = $data['shipment_element']; $id = $data['shipment_jplugin_id']; if (!empty($this->_psType) and !$this->selectedThis ($this->_psType, $name, $id)) { return FALSE; } if (isset($data['rules1'])) { // Try to parse all rules (and spit out error) to inform the user: $method = new StdClass (); $this->parseMethodRule ($data['rules1'], isset($data['countries1'])?$data['countries1']:array(), $data['tax_id1'], $method); $this->parseMethodRule ($data['rules2'], isset($data['countries2'])?$data['countries2']:array(), $data['tax_id2'], $method); $this->parseMethodRule ($data['rules3'], isset($data['countries3'])?$data['countries3']:array(), $data['tax_id3'], $method); $this->parseMethodRule ($data['rules4'], isset($data['countries4'])?$data['countries4']:array(), $data['tax_id4'], $method); $this->parseMethodRule ($data['rules5'], isset($data['countries5'])?$data['countries5']:array(), $data['tax_id5'], $method); $this->parseMethodRule ($data['rules6'], isset($data['countries6'])?$data['countries6']:array(), $data['tax_id6'], $method); $this->parseMethodRule ($data['rules7'], isset($data['countries7'])?$data['countries7']:array(), $data['tax_id7'], $method); $this->parseMethodRule ($data['rules8'], isset($data['countries8'])?$data['countries8']:array(), $data['tax_id8'], $method); } $ret=$this->setOnTablePluginParams ($name, $id, $table); return $ret; } } if (class_exists ('ShippingRule')) { return; } class ShippingRule { var $rulestring = ''; var $countries = array(); var $tax_id = 0; var $conditions = array(); var $shipping = 0; var $includes_tax = 0; var $name = ''; function __construct ($rule, $countries, $tax_id) { if (is_array($countries)) { $this->countries = $countries; } elseif (!empty($countries)) { $this->countries[0] = $countries; } $this->tax_id = $tax_id; $this->rulestring = $rule; $this->parseRule($rule); } function parseRule($rule) { $ruleparts=explode(';', $rule); foreach ($ruleparts as $p) { $this->parseRulePart($p); } } function handleAssignment ($var, $value, $rulepart) { switch (strtolower($var)) { case 'shipping': $this->shipping = $value; $this->includes_tax = False; break; case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; break; case 'name': $this->name = $value; break; default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error'); } } function parseRulePart($rulepart) { /* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */ /* In the advanced version, all conditions and costs can be given as a full mathematical expression */ /* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */ $operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '=='); $op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/'; $rulepart = trim($rulepart); if (empty($rulepart)) return; $atoms = preg_split ($op_re, $rulepart, -1, PREG_SPLIT_DELIM_CAPTURE); if (count($atoms)==1) { $this->shipping = $this->parseShippingTerm($atoms[0]); } elseif ($atoms[1]=='=') { $this->handleAssignment ($atoms[0], $atoms[2], $rulepart); } else { // Conditions, need at least three atoms! while (count($atoms)>1) { if (in_array ($atoms[1], $operators)) { $this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2])); array_shift($atoms); array_shift($atoms); } else { JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $rulepart), 'error'); $atoms = array(); } } } } function parseShippingTerm($expr) { /* In the advanced version, shipping cost can be given as a full mathematical expression */ return strtolower($expr); } function evaluateComparison ($terms, $vals) { while (count($terms)>2) { $res = false; switch ($terms[1]) { case '<': $res = ($terms[0] < $terms[2]); break; case '<=': case '=<': $res = ($terms[0] <= $terms[2]); break; case '==': $res = ($terms[0] == $terms[2]); break; case '!=': case '<>': $res = ($terms[0] != $terms[2]); break; case '>=': case '=>': $res = ($terms[0] >= $terms[2]); break; case '>': $res = ($terms[0] > $terms[2]); break; case '~': $l=min(strlen($terms[0]), strlen($terms[2])); $res = (strncmp ($terms[0], $terms[2], $l) == 0); break; default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $terms[1], $this->rulestring), 'error'); $res = false; } if ($res==false) return false; array_shift($terms); array_shift($terms); } if (count($terms)>1) { // We do not have the correct number of terms for chained comparisons, i.e. two terms leftover instead of one! JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error'); return false; } // All conditions were fulfilled, so we can return true return true; } function evaluateFunction ($function, $args) { $func = strtolower($function); // Functions with no argument: if (count($args) == 0) { $dt = getdate(); switch ($func) { case "second": return $dt['seconds']; break; case "minute": return $dt['minutes']; break; case "hour": return $dt['hours']; break; case "day": return $dt['mday']; break; case "weekday":return $dt['wday']; break; case "month": return $dt['mon']; break; case "year": return $dt['year']; break; case "yearday":return $dt['yday']; break; } } // Functions with exactly one argument: if (count($args) == 1) { switch ($func) { case "round": return round($args[0]); break; case "ceil": return ceil ($args[0]); break; case "floor": return floor($args[0]); break; case "abs": return abs($args[0]); break; case "not": return !$args[0]; break; } } // Functions with variable number of args switch ($func) { case "max": return max($args); break; case "min": return min($args); break; case "list": case "array": return $args; break; } // No known function matches => print an error, return 0 JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION', $function, $this->rulestring), 'error'); return 0; } function evaluateTerm ($expr, $vals) { if (is_null($expr)) { return $expr; } elseif (is_numeric ($expr)) { return $expr; } elseif (is_string ($expr)) { // Explicit strings are delimited by '...' or "..." if (($expr[0]=='\'' || $expr[0]=='"') && ($expr[0]==substr($expr,-1)) ) { return substr($expr,1,-1); } elseif (array_key_exists(strtolower($expr), $vals)) { return $vals[strtolower($expr)]; } else { JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error'); return null; } } elseif (is_array($expr)) { // Operator $op = array_shift($expr); $args = array(); $evaluate = true; if ($op == "FUNCTION") { $evaluate = false; } foreach ($expr as $e) { $term = $evaluate ? ($this->evaluateTerm($e, $vals)) : $e; if ($op == 'comparison') { // For comparisons, we only evaluate every other term (the operators are NOT evaluated!) $evaluate = !$evaluate; } if ($op == "FUNCTION") { $evaluate = true; } if (is_null($term)) return null; $args[] = $term; } $res = false; switch ($op) { // Logical operators: case 'OR': foreach ($args as $a) { $res = ($res || $a); }; break; case '&&': case 'AND': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break; case 'in': $res = in_array($args[0], $args[1]); break; // Comparisons: case '<': case '<=': case '=<': case '==': case '!=': case '<>': case '>=': case '=>': case '>': case '~': $res = $this->evaluateComparison(array($args[0], $op, $args[1]), $vals); break; case 'comparison': $res = $this->evaluateComparison($args, $vals); break; // Unary operators: case '.-': $res = -$args[0]; break; case '.+': $res = $args[0]; break; // Binary operators case "+": $res = ($args[0] + $args[1]); break; case "-": $res = ($args[0] - $args[1]); break; case "*": $res = ($args[0] * $args[1]); break; case "/": $res = ($args[0] / $args[1]); break; case "%": $res = (fmod($args[0], $args[1])); break; case "^": $res = ($args[0] ^ $args[1]); break; // Functions: case "FUNCTION": $func = array_shift($args); $res = $this->evaluateFunction($func, $args); break; default: $res = false; } // JFactory::getApplication()->enqueueMessage("<pre>Result of ".print_r($expr,1)." is $res.</pre>", 'error'); return $res; } else { // Neither string nor numeric, nor operator... JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error'); return null; } } function calculateShipping ($vals) { return $this->evaluateTerm($this->shipping, $vals); } function matches($vals) { // First, check the country, if any conditions are given: if (count ($this->countries) > 0 && !in_array ($vals['countryid'], $this->countries)) { // vmdebug('Rule::matches: Country check failed: countryid='.print_r($vals['countryid'],1).', countries are: '.print_r($this->countries,1).'...'); return False; } foreach ($this->conditions as $c) { // All conditions have to match! $ret = $this->evaluateTerm($c, $vals); // JFactory::getApplication()->enqueueMessage("Evaluating term <pre>".print_r($c,1)."</pre> returns $ret", 'error'); if (is_null($ret) || (!$ret)) { return false; } } // All conditions match, so return true return true; } function getRuleName($vals) { // Replace all {variable} tags in the name by the variables from $vals $matches=array(); $name=$this->name; preg_match_all('/{([A-Za-z0-9_]+)}/', $name, $matches); foreach ($matches[1] as $m) { $var=strtolower($m); if (isset($vals[$var])) { $name = str_replace("{".$m."}", strval($vals[$var]), $name); } } return $name; } function getShippingCosts($vals) { return $this->calculateShipping($vals); } function isNoShipping() { // NoShipping is set, so if the rule matches, this method should not offer any shipping at all return (is_string($this->shipping) && (strtolower($this->shipping)=="noshipping")); } } // No closing tag