diff --git a/Makefile b/Makefile index cbbb226bd8cdded5a113ecc5183c5fc1305aa81d..37b7df01ce870a207ed359c6f6609425f4125be9 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ BASE=rules_shipping BASE_ADV=rules_shipping_advanced PLUGINTYPE=vmshipment -VERSION=1.1.0 +VERSION=2.0 -PLUGINFILES=$(BASE).php $(BASE).script.php $(BASE).xml index.html -PLUGINFILES_ADV=$(BASE_ADV).php $(BASE).php $(BASE_ADV).script.php $(BASE_ADV).xml index.html +PLUGINFILES=$(BASE).php $(BASE)_base.php $(BASE).script.php $(BASE).xml index.html +PLUGINFILES_ADV=$(BASE_ADV).php $(BASE)_base.php $(BASE_ADV).script.php $(BASE_ADV).xml index.html TRANSLATIONS=$(call wildcard,language/*/*.plg_$(PLUGINTYPE)_$(BASE).*ini) TRANSLATIONS_ADV=$(subst $(BASE),$(BASE_ADV),$(TRANSLATIONS)) diff --git a/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini b/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini index 61c468515cbceb56f8a25e55541c1e7e138d3c72..6e55886577c071bddbc47ed7176ba29d1f6734f4 100644 --- a/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini +++ b/language/en-GB/en-GB.plg_vmshipment_rules_shipping.ini @@ -60,4 +60,6 @@ VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED="Error during parsing expression '%s': A VMSHIPMENT_RULES_EVALUATE_NONNUMERIC="Encountered term '%s' during evaluation, that does not evaluate to a numeric value! (Full rule: '%s')" VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR="Syntax error during evaluation, RPN is not well formed! (Full rule: '%s')" VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR="Unknown operator '%s' encountered during evaluation of rule '%s'." -VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR="Unknown error occurred during evaluation of rule '%s'." \ No newline at end of file +VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR="Unknown error occurred during evaluation of rule '%s'." +VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL="Assignments are not allows inside expressions (rule given was '%s')" +VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE="Evaluation yields unknown value while evaluating rule part '%s'." \ No newline at end of file diff --git a/releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip b/releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..41e4dc7729a16aa1bd6dfb4e9526f4d93c36923a Binary files /dev/null and b/releases/plg_vmshipment_rules_shipping_advanced_v2.0.zip differ diff --git a/releases/plg_vmshipment_rules_shipping_v2.0.zip b/releases/plg_vmshipment_rules_shipping_v2.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..83ec336faa2c9b2e7db00a3b9698b3e5f975ca93 Binary files /dev/null and b/releases/plg_vmshipment_rules_shipping_v2.0.zip differ diff --git a/rules_shipping.php b/rules_shipping.php index 6c3265d4092351f58bcbf81f278b3e28b74dd7d8..f042ae12406cd2c9331dc3a418d3b4abcfe5694d 100644 --- a/rules_shipping.php +++ b/rules_shipping.php @@ -24,619 +24,20 @@ defined ('_JEXEC') or die('Restricted access'); if (!class_exists ('vmPSPlugin')) { require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); } -if (!class_exists ('plgVmShipmentRules_Shipping')) { -// Only declare the class once... + +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { + require (dirname(__FILE__).DS.'rules_shipping_base.php'); +} /** 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 extends vmPSPlugin { - - /** - * @param object $subject - * @param array $config - */ +class plgVmShipmentRules_Shipping extends plgVmShipmentRules_Shipping_Base { 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=" (".$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 (empty($method->rules)) $this->parseMethodRules($method); - $cartvals = $this->getCartValues ($cart, $cart_prices); - - foreach ($method->rules as $r) { - if ($r->matches($cartvals)) { - $method->tax_id = $r->tax_id; - $method->matched_rule = $r; - $method->rule_name = $r->name; - $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 (!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 (); - } - - 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 . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true)); - $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'); - } - $cart_prices[$this->_psType . '_calc_id'] = $taxrules[0]['virtuemart_calc_id']; - } else { - $cart_prices['salesPrice' . $_psType] = $value; - $cart_prices[$this->_psType . 'Tax'] = 0; - $cart_prices[$this->_psType . '_calc_id'] = 0; - } - } - - 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; - if (class_exists('ShippingRule_Advanced')) { - $method->rules[]=new ShippingRule_Advanced($r, $countries, $tax); - } else { - $method->rules[]=new ShippingRule($r, $countries, $tax); - } - } - } - - protected function parseMethodRules (&$method) { - $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 getOrderDimensions (VirtueMartCart $cart) { - /* 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, - 'maxwidth' => 0, 'minwidth' => 9999999999, - 'maxheight' => 0, 'minheight' => 9999999999, - ); - if ($calculated==0) { - $calculated=1; - foreach ($cart->products as $product) { - $volume = $product->product_length * $product->product_width * $product->product_height; - $dimensions['volume'] += $volume * $product->quantity; - $dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume); - $dimensions['minvolume'] = min ($dimensions['minvolume'], $volume); - $dimensions['maxlength'] = max ($dimensions['maxlength'], $product->product_length); - $dimensions['minlength'] = min ($dimensions['minlength'], $product->product_length); - $dimensions['maxwidth'] = max ($dimensions['maxwidth'], $product->product_width); - $dimensions['minwidth'] = min ($dimensions['minwidth'], $product->product_width); - $dimensions['maxheight'] = max ($dimensions['maxheight'], $product->product_height); - $dimensions['minheight'] = min ($dimensions['minheight'], $product->product_height); - $articles += $product->quantity; - } - } - return $dimensions; - } - - 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 getCartValues (VirtueMartCart $cart, $cart_prices) { - $orderWeight = $this->getOrderWeight ($cart, $method->weight_unit); - $dimensions = $this->getOrderDimensions ($cart); - $address = (($cart->ST == 0) ? $cart->BT : $cart->ST); - - $products = 0; - $articles = 0; - foreach ($cart->products as $product) { - $products += 1; - $articles += $product->quantity; - } - $cartvals = array('weight'=>$orderWeight, - 'zip'=>$address['zip'], - 'articles'=>$articles, - 'products'=>$products, - 'amount'=>$cart_prices['salesPrice'], - 'country'=>$address['virtuemart_country_id'], - 'volume' => $dimensions['volume'], - 'maxvolume' => $dimensions['maxvolume'], - 'minvolume' => $dimensions['minvolume'], - 'maxlength' => $dimensions['maxlength'], - 'minlength' => $dimensions['minlength'], - 'maxwidth' => $dimensions['maxwidth'], - 'minwidth' => $dimensions['minwidth'], - 'maxheight' => $dimensions['maxheight'], - 'minheight' => $dimensions['minheight'] - ); - return $cartvals; - } - - - /** - * @param \VirtueMartCart $cart - * @param int $method - * @param array $cart_prices - * @return bool - */ - protected function checkConditions ($cart, $method, $cart_prices) { - if (empty($method->rules)) $this->parseMethodRules($method); - - $cartvals = $this->getCartValues ($cart, $cart_prices); - foreach ($method->rules as $r) { - if ($r->matches($cartvals)) { - $method->matched_rule = $r; - $method->rule_name = $r->name; - return TRUE; - } - } - vmdebug('checkConditions '.$method->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); - } - - - /** - * @author Max Milbers - * @param $data - * @param $table - * @return bool - */ - 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; - } else { - // Try to parse all rules (and spit out error) to inform the user: - $method = new StdClass (); - $this->parseMethodRule ($data['rules1'], $data['countries1'], $data['tax_id1'], $method); - $this->parseMethodRule ($data['rules2'], $data['countries2'], $data['tax_id2'], $method); - $this->parseMethodRule ($data['rules3'], $data['countries3'], $data['tax_id3'], $method); - $this->parseMethodRule ($data['rules4'], $data['countries4'], $data['tax_id4'], $method); - $this->parseMethodRule ($data['rules5'], $data['countries5'], $data['tax_id5'], $method); - $this->parseMethodRule ($data['rules6'], $data['countries6'], $data['tax_id6'], $method); - $this->parseMethodRule ($data['rules7'], $data['countries7'], $data['tax_id7'], $method); - $this->parseMethodRule ($data['rules8'], $data['countries8'], $data['tax_id8'], $method); - return $this->setOnTablePluginParams ($name, $id, $table); - } - } - - -} - -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 handleAssignment ($variable, $value, $rulepart) { - switch ($variable) { - case 'shipping': $this->shipping = $this->parseShippingTerm($value); break; - case 'shippingwithtax': $this->shipping = $this->parseShippingTerm($value); $this->includes_tax = True; break; - case 'name': $this->name = $value; break; - default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $variable, $rulepart), 'error'); - } - } - function parseRule($rule) { - $ruleparts=explode(';', $rule); - $operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '=='); - $op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/'; - foreach ($ruleparts as $p) { - $p = trim($p); - if (empty($p)) continue; - $atoms = preg_split ($op_re, $p, -1, PREG_SPLIT_DELIM_CAPTURE); - if (count($atoms)==1) { - $this->shipping = $this->parseShippingTerm($atoms[0]); - } elseif ($atoms[1]=='=') { - $this->handleAssignment (strtolower($atoms[0]), $atoms[2], $p); - } 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], $p), 'error'); - $atoms = array(); - } - } - } - } - } - - function parseShippingTerm($expr) { - /* In the advanced version, shipping cost can be given as a full mathematical expression */ - return strtolower($expr); - } - - function evaluateTerm ($expr, $vals) { - /* In the advanced version, all conditions and costs can be given as a full mathematical expression */ - $e=$expr; - // Strings indicate members of the $vals array (weight, amount, etc.) - if (!is_numeric($e) && isset($vals[$e])) $e=$vals[$e]; - if (!is_numeric($e)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error'); - $e = 0; - } - return $e; - } - - 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['country'], $this->countries)) - return False; - - foreach ($this->conditions as $c) { - $v1=$this->evaluateTerm($c[1], $vals); - $v2=$this->evaluateTerm($c[2], $vals); - // Unknown strings / unevaluatable terms cause the rule to NOT match: - if (!is_numeric($v1) || !is_numeric($v2)) { - return false; - } - $res = false; - switch ($c[0]) { - case '<': $res = ($v1<$v2); break; - case '<=': - case '=<': $res = ($v1<=$v2); break; - case '==': $res = ($v1==$v2); break; - case '!=': - case '<>': $res = ($v1!=$v2); break; - case '>=': - case '=>': $res = ($v1>=$v2); break; - case '>': $res = ($v1>$v2); break; - } - // Immediately return, if this rule does not match - if (!$res) return false; - } - // All conditions match, so return true - return true; - } - - function getShippingCosts($vals) { - return $this->calculateShipping($vals); } } -} // No closing tag diff --git a/rules_shipping.xml b/rules_shipping.xml index a1805d4165b84d1a1da75fc1ae24db1d575c44bf..3c30b2885c5285a9700d6957f45a1ca92ebc327a 100644 --- a/rules_shipping.xml +++ b/rules_shipping.xml @@ -1,15 +1,16 @@ <?xml version="1.0" encoding="UTF-8" ?> <install version="1.5" type="plugin" group="vmshipment" method="upgrade"> <name>VMSHIPMENT_RULES</name> - <creationDate>2013-01-12</creationDate> + <creationDate>2013-02-08</creationDate> <author>Reinhold Kainhofer</author> <authorUrl>http://www.kainhofer.com</authorUrl> <copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright> <license>GPL v3+</license> - <version>1.1.0</version> + <version>2.0.0</version> <description>VMSHIPMENT_RULES_DESC</description> <files> <filename plugin="rules_shipping">rules_shipping.php</filename> + <filename>rules_shipping_base.php</filename> <folder>language</folder> <folder>elements</folder> </files> diff --git a/rules_shipping_advanced.php b/rules_shipping_advanced.php index d651d1aadc39efce3e704c82da27e8d5a6d37ad7..92b92bc17e198bbbb0b52e80dd05182e0ff78eb5 100644 --- a/rules_shipping_advanced.php +++ b/rules_shipping_advanced.php @@ -25,28 +25,98 @@ defined ('_JEXEC') or die('Restricted access'); if (!class_exists ('vmPSPlugin')) { require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php'); } -if (!class_exists ('plgVmShipmentRules_Shipping')) { - require (dirname(__FILE__).DS.'rules_shipping.php'); +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { + require (dirname(__FILE__).DS.'rules_shipping_base.php'); } /** Shipping costs according to general rules. * Derived from the standard plugin, no need to change anything! The standard plugin already uses the advanced rules class defined below, if it can be found */ -class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping { -// function __construct (& $subject, $config) { -// parent::__construct ($subject, $config); -// } +class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_Base { + function __construct (& $subject, $config) { + parent::__construct ($subject, $config); + } + protected function createMethodRule ($r, $countries, $tax) { + return new ShippingRule_Advanced ($r, $countries, $tax); + } + /** Allow child classes to add additional variables for the rules + */ + protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) { + $values['coupon'] = $cart->couponCode; + } + +} + +// is_comparison: if the expression $o is a (possibly chained) comparison consisting of operators in $ops, +// return the last term of hte comparison; null otherwise +function is_comparison ($o, $ops) { + if (!is_array($o)) + return null; + if (in_array($o[0], $ops)) + return $o[2]; + $o2comp = is_comparison($o[2], $ops); + if (trim($o[0])=='AND' && !is_null (is_comparison($o[1], $ops)) && !is_null($o2comp)) + return $o2comp; + return null; } - + + /** Extend the shipping rules by allowing arbitrary mathematical expressions */ class ShippingRule_Advanced extends ShippingRule { - var $operators = array("+"=>10, "-"=>10, "*"=>20, "/"=>20, "%"=>30, "^"=>50, "("=>100, ")"=>100); + var $operators = array( + "^" => 70, + "*" => 60, "/" => 60, "%" => 60, + "+" => 50, "-" => 50, + "<" => 40, "<=" => 40, ">" => 40, ">=" => 40, "=>" => 40, "=<" => 40, + "==" => 40, "!=" => 40, "<>" => 40, + "&" => 21, + " OR " => 20, + "=" => 10, + + "(" => 0, ")" =>0 ); function __construct ($rule, $countries, $tax_id) { parent::__construct ($rule, $countries, $tax_id); } + + function tokenize_expression ($expression) { + // First, extract all strings, delimited by ": + $str_re = '/("(?:\\"|[^"])*"|\'(?:\\\'|[^\'])*\')/'; + $strings = preg_split($str_re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + // Then split all other parts of the expression at the operators + $op_re = ':\s*( OR |<=|=>|>=|=>|<>|!=|==|<|=|>|\+|-|\*|/|%|\(|\)|\^)\s*:'; + $atoms = array(); + foreach ($strings as $s) { + if (preg_match($str_re, $s)) { + $atoms[] = $s; + } else { + $newatoms = preg_split($op_re, $s, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + $atoms = array_merge ($atoms, $newatoms); + } + } + // Finally handle the double meaning of -: After an operator and before an operand, it is NOT an operator, but belongs to the operand + $prevop = true; + $result = array(); + $leftover = ''; + foreach ($atoms as $a) { + if ($a == "-") { + $prev = end($result); + if (is_null ($prev) || (preg_match ($op_re, $prev) && $prev != ')')) { + $leftover = $a; + } else { + $result[] = $leftover.$a; + $leftover = ''; + } + } else { + $result[] = $leftover.$a; + $leftover = ''; + } + } + return $result; + } + /** parse the mathematical expressions (following Knuth 1962): * First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators) @@ -63,50 +133,74 @@ class ShippingRule_Advanced extends ShippingRule { * 6a) Pop operators from stack until opening parenthesis is found * 6b) push them to the result (not the opening parenthesis, of course) * 7) At the end of the input, pop all operators from the stack and onto the result - * Store this RPN list for later evaluation + * + * Afterwards, convert this RPN list into an expression tree to be evaluate + * */ - function parseShippingTerm ($expr) { - $op_re = ':\s*(\+|-|\*|/|%|\(|\)|\^)\s*:'; - $atoms = preg_split($op_re, strtolower($expr), -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + 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 */ + $rulepart = trim($rulepart); + if (empty($rulepart)) return; + + + // Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression! + if (preg_match('/^\s*(name)\s*=\s*"?(.*)"?\s*$/i', $rulepart, $matches)) { + $this->handleAssignment ($matches[1], $matches[2], $rulepart); + return; + } + + // Split at all operators: + $atoms = $this->tokenize_expression ($rulepart); + + // Any of these indicate a comparison and thus a condition: + $comparison_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>'); + $is_comparison = false; + $is_assignment = false; + if (count($atoms)==1) return $atoms[0]; - // Operators,including precedence $stack = array (); // 1) - $result = array (); + $rpn = array (); foreach ($atoms as $a) { // 2) - if (!isset($this->operators[$a])) { // 3) - array_push ($result, $a); - } elseif ($a == "(") { // 5) + if (!isset($this->operators[$a])) { // 3) Operand + array_push ($rpn, $a); + } elseif ($a == "(") { // 5) parenthesis array_push ($stack, $a); - } elseif ($a == ")") { // 6) + } elseif ($a == ")") { // 6) parenthesis do { $op=array_pop($stack); // 6a) - if (!empty($op) && $op != "(") { - array_push ($result, $op); // 6b) + if (!is_null($op) && ($op != "(")) { + array_push ($rpn, $op); // 6b) } else { if ($op != "(") { // If no ( can be found, the expression is wrong! - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $expr), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error'); } break; // We have found the opening parenthesis } - } while (0); - } else { // 4 + } while (true); + } else { // 4) operators // For operators, pop operators from the stack until you reach an opening parenthesis, // an operator of lower precedence, or a right associative symbol of equal precedence. $prec = $this->operators[$a]; + $is_comparison |= in_array($a, $comparison_ops); + $is_assignment |= ($a == "="); while (count($stack)>0) { // 4a) $op = array_pop ($stack); - // Ignore the right-associative symbols of equal precedence for now... + // The only right-associative operator is =, which we allow at most once! if ($op == "(") { + // add it back to the stack! + array_push ($stack, $op); break; } elseif ($this->operators[$op]<$prec) { // We found an operator with lower precedence, add it back to the stack! array_push ($stack, $op); // 4b) break; } else { - array_push ($result, $op); + array_push ($rpn, $op); } } while (0); array_push ($stack, $a); // 4b) @@ -116,27 +210,13 @@ class ShippingRule_Advanced extends ShippingRule { while ($op=array_pop($stack)) { // Opening parentheses should not be found on the stack any more. That would mean a closing paren is missing! if ($op == "(") { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $expr), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error'); } else { - array_push ($result, $op); + array_push ($rpn, $op); } } - /* In the advanced version, shipping cost can be given as a full mathematical expression */ - return $result; - } - function evaluateTerm ($expr, $vals) { - if (!is_array($expr)) { - // Normal expression, no complex mathematical formula in RPN - if (!is_numeric($expr) && isset($vals[$expr])) $expr=$vals[$expr]; - if (!is_numeric($expr)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $expr, $this->rulestring), 'error'); - return 0; - } - return $expr; - } - - /** Evaluate the RPN, according to Knuth: + /** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure, according to Knuth: * 1) Initialize an empty stack * 2) Read the RPN from left to right * 3) If operand, push it onto the stack @@ -148,49 +228,58 @@ class ShippingRule_Advanced extends ShippingRule { * 5) At the end of the RPN, pop the result from the stack. * 5a) The stack should now be empty (otherwise, ERROR, invalid syntax) */ + $stack=array(); // 1) - foreach ($expr as $e) { // 2) + foreach ($rpn as $e) { // 2) if (!isset($this->operators[$e])) { // 3) // Operand => push onto stack - if (!is_numeric($e) && isset($vals[$e])) $e = $vals[$e]; - if (!is_numeric($e)) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error'); - $e = 0; - } array_push ($stack, $e); } else { // 4) // Operator => apply to the last two values on the stack if (count($stack)<2) { // 4d) - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $this->rulestring), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error'); array_push($stack, 0); continue; } $o2 = array_pop($stack); // 4a) $o1 = array_pop($stack); - $res = 0; - switch ($e) { // 4b) - case "+": $res = $o1+$o2; break; - case "-": $res = $o1-$o2; break; - case "*": $res = $o1*$o2; break; - case "/": $res = $o1/$o2; break; - case "%": $res = $o1%$o2; break; - case "^": $res = $o1^$o1; break; - default: - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR', $e, $this->rulestring), 'error'); - $res = 0; + // TODO: Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison, + // insert AND(o1, e(arg2(o1), o2)) instead of e(o1, o1) + // is_comparison nicely returns the last term of the (possibly chained) comparison, null otherwise + $o1comp = is_comparison($o1, $comparison_ops); + if (in_array ($e, $comparison_ops) && !is_null($o1comp) ) { + $op = array ('AND', $o1, array($e, $o1comp, $o2)); + } else { + $op = array ($e, $o1, $o2); // 4b) } - array_push($stack, $res); // 4c) + array_push ($stack, $op); // 4c) } } // 5a) if (count($stack) != 1) { - JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error'); + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $rulepart), 'error'); $stack = array (0); } $res = array_pop($stack); // 5) - return $res; + + if ($is_comparison) { // Comparisons are conditions + $this->conditions[] = $res; + } elseif ($is_assignment) { + + if ($res[0]=='=') { + $this->handleAssignment ($res[1], $res[2], $rulepart); + } else { + // Assignment has to be top-level! + JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL', $rulepart), 'error'); + } + } else { + // Terms without comparisons or assignments are shipping cost expressions + $this->shipping = $res; + $this->includes_tax = False; + } } + } // No closing tag diff --git a/rules_shipping_advanced.xml b/rules_shipping_advanced.xml index 71fe47561f2019c9736cb971e6cdfd253518688e..278d8edabe6cf4524332cabf8c16cba5d7445eb8 100644 --- a/rules_shipping_advanced.xml +++ b/rules_shipping_advanced.xml @@ -1,16 +1,16 @@ <?xml version="1.0" encoding="UTF-8" ?> <install version="1.5" type="plugin" group="vmshipment" method="upgrade"> <name>VMSHIPMENT_RULES_ADV</name> - <creationDate>2013-01-16</creationDate> + <creationDate>2013-02-08</creationDate> <author>Reinhold Kainhofer</author> <authorUrl>http://www.kainhofer.com</authorUrl> <copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright> <license>GPL v3+</license> - <version>1.1.0</version> + <version>2.0.0</version> <description>VMSHIPMENT_RULES_ADV_DESC</description> <files> <filename plugin="rules_shipping_advanced">rules_shipping_advanced.php</filename> - <filename>rules_shipping.php</filename> + <filename>rules_shipping_base.php</filename> <folder>language</folder> <folder>elements</folder> </files> diff --git a/rules_shipping_base.php b/rules_shipping_base.php new file mode 100644 index 0000000000000000000000000000000000000000..c42290ddc856306500c1981cc12ab8954441eabf --- /dev/null +++ b/rules_shipping_base.php @@ -0,0 +1,694 @@ +<?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'); +} +if (!class_exists ('plgVmShipmentRules_Shipping_Base')) { +// Only declare the class once... + +/** 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=" (".$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 (empty($method->rules)) $this->parseMethodRules($method); + $cartvals = $this->getCartValues ($cart, $method, $cart_prices); + + foreach ($method->rules as $r) { + if ($r->matches($cartvals)) { + $method->tax_id = $r->tax_id; + $method->matched_rule = $r; + $method->rule_name = $r->name; + $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 (!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 (); + } + + 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 . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true)); + $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'); + } + $cart_prices[$this->_psType . '_calc_id'] = $taxrules[0]['virtuemart_calc_id']; + } else { + $cart_prices['salesPrice' . $_psType] = $value; + $cart_prices[$this->_psType . 'Tax'] = 0; + $cart_prices[$this->_psType . '_calc_id'] = 0; + } + } + 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) { + $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 getOrderDimensions (VirtueMartCart $cart) { + /* 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, + 'maxwidth' => 0, 'minwidth' => 9999999999, + 'maxheight' => 0, 'minheight' => 9999999999, + ); + if ($calculated==0) { + $calculated=1; + foreach ($cart->products as $product) { + $volume = $product->product_length * $product->product_width * $product->product_height; + $dimensions['volume'] += $volume * $product->quantity; + $dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume); + $dimensions['minvolume'] = min ($dimensions['minvolume'], $volume); + $dimensions['maxlength'] = max ($dimensions['maxlength'], $product->product_length); + $dimensions['minlength'] = min ($dimensions['minlength'], $product->product_length); + $dimensions['maxwidth'] = max ($dimensions['maxwidth'], $product->product_width); + $dimensions['minwidth'] = min ($dimensions['minwidth'], $product->product_width); + $dimensions['maxheight'] = max ($dimensions['maxheight'], $product->product_height); + $dimensions['minheight'] = min ($dimensions['minheight'], $product->product_height); + } + } + return $dimensions; + } + + 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; + } + + /** 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) { + $orderWeight = $this->getOrderWeight ($cart, $method->weight_unit); + $address = (($cart->ST == 0) ? $cart->BT : $cart->ST); + + $products = 0; + $articles = 0; + foreach ($cart->products as $product) { + $products += 1; + $articles += $product->quantity; + } + $cartvals = array('weight'=>$orderWeight, + 'zip'=>$address['zip'], + 'articles'=>$articles, + 'products'=>$products, + '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'], + 'discountBeforeTaxBill'=>$cart_prices['discountBeforeTaxBill'], + + 'country'=>$address['virtuemart_country_id'], + ); + $cartvals = array_merge ($cartvals, $this->getOrderDimensions ($cart)); + // Let child classes update the $cartvals array, or add new variables + $this->addCustomCartValues($cart, $cart_prices, $cartvals); + return $cartvals; + } + + + /** + * @param \VirtueMartCart $cart + * @param int $method + * @param array $cart_prices + * @return bool + */ + protected function checkConditions ($cart, $method, $cart_prices) { + if (empty($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->name; + return TRUE; + } + } + vmdebug('checkConditions '.$method->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); + } + + + /** + * @author Max Milbers + * @param $data + * @param $table + * @return bool + */ + 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; + } else { + // Try to parse all rules (and spit out error) to inform the user: + $method = new StdClass (); + $this->parseMethodRule ($data['rules1'], $data['countries1'], $data['tax_id1'], $method); + $this->parseMethodRule ($data['rules2'], $data['countries2'], $data['tax_id2'], $method); + $this->parseMethodRule ($data['rules3'], $data['countries3'], $data['tax_id3'], $method); + $this->parseMethodRule ($data['rules4'], $data['countries4'], $data['tax_id4'], $method); + $this->parseMethodRule ($data['rules5'], $data['countries5'], $data['tax_id5'], $method); + $this->parseMethodRule ($data['rules6'], $data['countries6'], $data['tax_id6'], $method); + $this->parseMethodRule ($data['rules7'], $data['countries7'], $data['tax_id7'], $method); + $this->parseMethodRule ($data['rules8'], $data['countries8'], $data['tax_id8'], $method); + $ret=$this->setOnTablePluginParams ($name, $id, $table); + return $ret; + } + } + + +} +} + +if (!class_exists ('ShippingRule')) { + +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 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(); + foreach ($expr as $e) { + $term = $this->evaluateTerm($e, $vals); + if (is_null($term)) return null; + $args[] = $term; + } + $res = false; + switch ($op) { + case 'OR': + case ' OR ': foreach ($args as $a) { $res = ($res || $a); }; break; + case 'AND': + case ' AND ': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break; + case 'in': $needle = array_shift($args); $res = in_array($needle, $args); break; + + case '<': $res = ($args[0] < $args[1]); break; + case '<=': + case '=<': $res = ($args[0] <= $args[1]); break; + case '==': $res = ($args[0] == $args[1]); break; + case '!=': + case '<>': $res = ($args[0] != $args[1]); break; + case '>=': + 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 = ($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; + + default: $res = false; + } + 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['country'], $this->countries)) + return False; + + foreach ($this->conditions as $c) { + // All conditions have to match! + $ret = $this->evaluateTerm($c, $vals); + if (is_null($ret) || (!$ret)) { + return false; + } + } + // All conditions match, so return true + return true; + } + + function getShippingCosts($vals) { + return $this->calculateShipping($vals); + } + +} + +} +// No closing tag